diff --git a/.gitattributes b/.gitattributes index 14bba0a3eb..e96e0676ca 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,15 +1,9 @@ -mobile/lib/**/*.g.dart -diff -merge -mobile/lib/**/*.g.dart linguist-generated=true - mobile/android/**/*.g.kt -diff -merge mobile/android/**/*.g.kt linguist-generated=true mobile/ios/**/*.g.swift -diff -merge mobile/ios/**/*.g.swift linguist-generated=true -mobile/lib/**/*.drift.dart -diff -merge -mobile/lib/**/*.drift.dart linguist-generated=true - mobile/drift_schemas/main/drift_schema_*.json -diff -merge mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index d509b404a1..2c6d889d7b 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -94,7 +94,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} working_directory: ./mobile @@ -216,7 +216,7 @@ jobs: persist-credentials: false - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} working_directory: ./mobile diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index 7cbab78069..a38f131a71 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@4bdf89062dad92caea71236f59704b04d3ce9682 # v0.1.8 + uses: oasdiff/oasdiff-action/breaking@0ab8ad204b00d25acc5ae87106281433e288d0c1 # v0.1.10 with: base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json @@ -43,7 +43,7 @@ jobs: persist-credentials: false - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ github.token }} working_directory: ./mobile diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index aa2c6297b2..7cfa4bbc92 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -44,7 +44,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -83,7 +83,7 @@ jobs: uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Login to GitHub Container Registry - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 if: ${{ !github.event.pull_request.head.repo.fork }} with: registry: ghcr.io diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9bf5abfcba..7e14227a51 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@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 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@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/autobuild@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 # â„šī¸ 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@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ab1ec84136..657d61d647 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -61,7 +61,7 @@ jobs: suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -91,7 +91,7 @@ jobs: suffix: [''] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 09c340e0fe..6d7bd497d7 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -68,7 +68,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index a452e2ae51..b88e3f56dc 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -143,7 +143,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index eec66484d3..cdb3f83c96 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -31,7 +31,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index 4c97f3f16c..2638c31fca 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -30,7 +30,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index abbcfaebd0..5820a8b284 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -8,7 +8,6 @@ permissions: {} jobs: validate-release-label: - if: ${{ !startsWith(github.event.pull_request.head.ref, 'mergify/merge-queue/') }} runs-on: ubuntu-latest permissions: issues: write diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index c2664839e3..d924c84596 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -19,6 +19,6 @@ jobs: permission-contents: read permission-pull-requests: write - - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 + - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 with: repo-token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 05c6f82977..1bf9a61e8c 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -68,13 +68,13 @@ jobs: ref: main - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} # TODO move to mise - name: Install uv - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Bump version env: diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index a5eaf26181..9c6101e4e1 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -29,7 +29,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 8fe1e1265c..3004f0323f 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -64,7 +64,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} working_directory: ./mobile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9fb652f59..acf3a8d7f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -120,7 +120,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -152,7 +152,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -184,7 +184,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -229,7 +229,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -268,7 +268,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -297,7 +297,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -348,7 +348,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -382,7 +382,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -441,6 +441,7 @@ jobs: if: ${{ !cancelled() }} - name: Start Docker Compose + id: docker run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 if: ${{ !cancelled() }} @@ -448,13 +449,13 @@ jobs: env: VITEST_DISABLE_DOCKER_SETUP: true run: pnpm test - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Run e2e tests (maintenance) env: VITEST_DISABLE_DOCKER_SETUP: true run: pnpm test:maintenance - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Capture Docker logs if: always() @@ -519,6 +520,7 @@ jobs: if: ${{ !cancelled() }} - name: Docker build + id: docker run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 if: ${{ !cancelled() }} @@ -526,7 +528,7 @@ jobs: env: PLAYWRIGHT_DISABLE_WEBSERVER: true run: pnpm test:web - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Archive e2e test (web) results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -539,7 +541,7 @@ jobs: env: PLAYWRIGHT_DISABLE_WEBSERVER: true run: pnpm test:web:ui - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Archive ui test (web) results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -552,7 +554,7 @@ jobs: env: PLAYWRIGHT_DISABLE_WEBSERVER: true run: pnpm test:web:maintenance - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Archive maintenance tests (web) results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -604,7 +606,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} working_directory: ./mobile @@ -642,7 +644,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -674,7 +676,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -727,7 +729,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} @@ -785,7 +787,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@4d97fb30d9edd3ff0d735f411f607a35204eea79 # use-mise-action-v3.2.0 + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 with: github_token: ${{ steps.token.outputs.token }} diff --git a/.mergify.yml b/.mergify.yml deleted file mode 100644 index 226e26c4c0..0000000000 --- a/.mergify.yml +++ /dev/null @@ -1,2 +0,0 @@ -merge_queue: - status_comments: outcomes diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 399fedae33..782e0192f6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -12,6 +12,8 @@ "editorconfig.editorconfig", "foxundermoon.shell-format", "timonwong.shellcheck", - "bluebrown.yamlfmt" + "bluebrown.yamlfmt", + // TODO TS 7 while it's still in preview + "typescriptteam.native-preview" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 30dac3216e..23d4385964 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,6 +60,8 @@ "**/dist/**": true, "**/node_modules/**": true }, + // TODO remove once the ts7 extension is stable + "js/ts.experimental.useTsgo": true, "js/ts.preferences.importModuleSpecifier": "non-relative", "search.exclude": { "**/.svelte-kit": true, diff --git a/deployment/mise.lock b/deployment/mise.lock index 663ae4985d..018620a4a5 100644 --- a/deployment/mise.lock +++ b/deployment/mise.lock @@ -1,43 +1,43 @@ # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html [[tools.opentofu]] -version = "1.11.6" +version = "1.12.5" backend = "aqua:opentofu/opentofu" [tools.opentofu."platforms.linux-arm64"] -checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-arm64-musl"] -checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-x64"] -checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.linux-x64-musl"] -checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.macos-arm64"] -checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536399" +checksum = "sha256:2ae38150a667f5c0bd57b318d18ad8091d08f93fcca40345f3d88998661de5a9" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602544" [tools.opentofu."platforms.macos-x64"] -checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536384" +checksum = "sha256:1012d8f3d4567bcbcd1f2c7d766feca39a30bced32fb8be47e1887fbbee2456d" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602621" [tools.opentofu."platforms.windows-x64"] -checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406" +checksum = "sha256:af11850b496f3720e0184084c56d8b43aa74ea92d2338978bf368d70c96473f1" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_windows_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" [[tools.terragrunt]] version = "1.0.3" diff --git a/deployment/mise.toml b/deployment/mise.toml index 2e26da09f6..7098b79c99 100644 --- a/deployment/mise.toml +++ b/deployment/mise.toml @@ -1,6 +1,6 @@ [tools] terragrunt = "1.1.1" -opentofu = "1.12.4" +opentofu = "1.12.5" [tasks."tg:fmt"] run = "terragrunt hclfmt" diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index aecb6b1dad..013ff266fa 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.5-ubuntu@sha256:00396460e499415c828b7c298f19287c8a0f95e72412ee37ac11723655c2d6b9 + image: grafana/grafana:12.4.6-ubuntu@sha256:35c75489ff2e2e69c53977a180de491643d51fdb32108f9335317527978daed7 volumes: - grafana-data:/var/lib/grafana diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index c8ebeffbcd..5c34acdd9d 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -6,14 +6,18 @@ sidebar_position: 100 A config file can be provided as an alternative to the UI configuration. +:::note Interaction with the web UI +While the config file does not need to include all keys from the below example, specifying `IMMICH_CONFIG_FILE` will disable the ability to edit other properties from the Immich web UI. +::: + ### Step 1 - Create a new config file -In JSON format, create a new config file (e.g. `immich.json`) and put it in a location mounted in the container that can be accessed by Immich. +In JSON format, create a new config file (e.g. `immich-config.json`) and put it in a location mounted in the container that can be accessed by Immich. YAML-formatted config files are also supported. The default configuration looks like this:
-immich.json +immich-config.json ```json { @@ -250,6 +254,10 @@ So you can just grab it from there, paste it into a file and you're pretty much ### Step 2 - Specify the file location +:::note +If you have any `microservices` workers, they will also need to have the config file mounted to their container. +::: + In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config. For more information, refer to the [Environment Variables](/install/environment-variables.md) section. @@ -261,7 +269,7 @@ It is recommended to reuse this variable in your `docker-compose.yml`: ```yaml volumes: - - ./configuration.yml:${IMMICH_CONFIG_FILE} + - ./immich-config.json:${IMMICH_CONFIG_FILE} ``` ::: diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index dbfd2fb112..c10a858ed9 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -45,7 +45,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | | `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | -| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api | +| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` and `/admin/database-backups/start-restore` endpoints | `true` | server | api | \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index 384d3d11d7..3ce98dc2e1 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -1,13 +1,19 @@ --- -sidebar_position: 60 +sidebar_position: 70 --- -# Unraid +# Unraid [ Community ] + +:::note +This is a community contribution and not officially supported by the Immich team, but included here for convenience. + +Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/). +::: Immich can easily be installed and updated on Unraid via: -1. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps -2. Community made template on the Unraid Community Apps +1. Community made template on the Unraid Community Apps +2. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps ## Community Applications Template @@ -23,7 +29,7 @@ Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup) -## Docker-Compose Method (Official) +## Docker-Compose Method :::info diff --git a/docs/mise.toml b/docs/mise.toml index 1236b21160..c4fe5f1b68 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.111.0" +wrangler = "4.114.0" diff --git a/e2e/mise.toml b/e2e/mise.toml index b149922564..487f4ebf46 100644 --- a/e2e/mise.toml +++ b/e2e/mise.toml @@ -1,5 +1,5 @@ [tasks.install] -run = "pnpm install --filter immich-e2e --frozen-lockfile" +run = "pnpm install --filter immich-e2e... --frozen-lockfile" [tasks.build] dir = "{{ config_root }}" @@ -40,18 +40,19 @@ run = "tsc --noEmit" [tasks.ci-setup] -depends = [ - "//:sdk:install", - "//:sdk:build", - "//packages/cli:install", - "//packages/cli:build", +run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, + { task = "//packages/cli:install" }, + { task = "//packages/cli:build" }, + { task = ":install" }, ] -run = { task = ":install" } [tasks.ci-unit] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":format" }, { task = ":lint" }, diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 5fd887c44b..1b3447767e 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -38,9 +38,6 @@ export const errorDto = { incorrectLogin: { message: 'Incorrect email or password', }, - alreadyHasAdmin: { - message: 'The server already has an admin', - }, }; export const signupResponseDto = { diff --git a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts index cf6d752561..e757c721d6 100644 --- a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts +++ b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts @@ -108,7 +108,7 @@ describe('/admin/database-backups', () => { const { status, body } = await request(app).post('/admin/database-backups/start-restore').send(); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('The server already has an admin')); + expect(body).toEqual(errorDto.badRequest('Admin setup is not available')); }); it.sequential('should enter maintenance mode in "database restore mode"', async () => { diff --git a/i18n/en.json b/i18n/en.json index 7710bd1686..8b4917d244 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -30,6 +30,7 @@ "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "add_to_album_bottom_sheet_some_local_assets": "Some local assets could not be added to album", + "add_to_album_item_count": "Add {count, plural, one {# item} other {# items}} to album", "add_to_albums": "Add to albums", "add_to_albums_count": "Add to albums ({count})", "add_to_bottom_bar": "Add to", diff --git a/mise.lock b/mise.lock index 656d11b3eb..a4b395500b 100644 --- a/mise.lock +++ b/mise.lock @@ -82,6 +82,15 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133" version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"] +checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094" + +[[tools."github:jellyfin/jellyfin-ffmpeg"]] +version = "7.1.3-6" +backend = "github:jellyfin/jellyfin-ffmpeg" + [tools."github:jellyfin/jellyfin-ffmpeg".options] asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" @@ -111,23 +120,6 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets 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" - -[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-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" -backend = "github:jellyfin/jellyfin-ffmpeg" - [tools."github:jellyfin/jellyfin-ffmpeg".options] asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" @@ -140,10 +132,18 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"] -checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094" +[tools."github:jellyfin/jellyfin-ffmpeg".options] +asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-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-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:webassembly/binaryen"]] version = "version_124" @@ -252,82 +252,82 @@ version = "7.5.0" backend = "npm:oazapfts" [[tools.opentofu]] -version = "1.12.4" +version = "1.12.5" backend = "aqua:opentofu/opentofu" [tools.opentofu."platforms.linux-arm64"] -checksum = "sha256:dc7bfcd93ce9795a86c58fbf71efd013c39dcd1febb13c9cd3555c43b9c2403a" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646678" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-arm64-musl"] -checksum = "sha256:dc7bfcd93ce9795a86c58fbf71efd013c39dcd1febb13c9cd3555c43b9c2403a" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646678" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-x64"] -checksum = "sha256:81836d0f12b4fe9013b85586349f993def9429b6383bb77cdd6c2f3a9d9aac24" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646677" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.linux-x64-musl"] -checksum = "sha256:81836d0f12b4fe9013b85586349f993def9429b6383bb77cdd6c2f3a9d9aac24" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646677" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.macos-arm64"] -checksum = "sha256:7c06e4390d9ccd467773e37ff1c3d833c7ca0c24742cd9e9ad47284bea472247" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_darwin_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646544" +checksum = "sha256:2ae38150a667f5c0bd57b318d18ad8091d08f93fcca40345f3d88998661de5a9" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602544" [tools.opentofu."platforms.macos-x64"] -checksum = "sha256:ead1d2ce643addb4ffeb93240b9377ae1c2fd793a6bd22d65922ac37adfdf546" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_darwin_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646688" +checksum = "sha256:1012d8f3d4567bcbcd1f2c7d766feca39a30bced32fb8be47e1887fbbee2456d" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602621" [tools.opentofu."platforms.windows-x64"] -checksum = "sha256:a4d86a07755c8d151f20f945e6cfb5b40deeed942af36a9bd385c5c2e965d5dd" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_windows_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646543" +checksum = "sha256:af11850b496f3720e0184084c56d8b43aa74ea92d2338978bf368d70c96473f1" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_windows_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" [[tools.pnpm]] -version = "11.13.1" +version = "11.17.0" backend = "aqua:pnpm/pnpm" [tools.pnpm."platforms.linux-arm64"] -checksum = "sha256:b52db99d215ed7dc9563aed815953c62a6c1ffd7cd75803d3a07ad7e4f246aed" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563983" +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" provenance = "github-attestations" [tools.pnpm."platforms.linux-arm64-musl"] -checksum = "sha256:2cadd4fc815c591f498a0a84c9e74a836e3e8c1236275f6e1cfd355bae6ae957" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563985" +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" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64"] -checksum = "sha256:bd6d4b0e14536207ad76bc838f5980cecd968da15f69aae0b207380cca3f2e98" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563986" +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" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-musl"] -checksum = "sha256:ba19690f4ed1b64f1203ade14e9216352b46232d5582468b26a0160e0c9618c5" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563980" +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" provenance = "github-attestations" [tools.pnpm."platforms.macos-arm64"] -checksum = "sha256:765c2bf04e8129cb58c0f946e324262e418370b35a203b50b1f06a0567ef8bc1" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-darwin-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563984" +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" provenance = "github-attestations" [tools.pnpm."platforms.windows-x64"] -checksum = "sha256:d8bebbc71df2702961c1d34a5e61196bc0aa3bbde33c33253f6afa3dd4546a6d" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-win32-x64.zip" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563981" +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" provenance = "github-attestations" [[tools.terragrunt]] diff --git a/mise.toml b/mise.toml index 5ff9db5819..ad7fdf5a90 100644 --- a/mise.toml +++ b/mise.toml @@ -16,9 +16,9 @@ config_roots = [ [tools] node = "24.15.0" -pnpm = "11.13.1" +pnpm = "11.17.0" terragrunt = "1.1.1" -opentofu = "1.12.4" +opentofu = "1.12.5" "npm:@openapitools/openapi-generator-cli" = "2.40.1" "npm:oazapfts" = "7.5.0" "github:extism/cli" = "1.6.3" @@ -69,7 +69,7 @@ sources = [ "patch/*", "bin/generate-dart-sdk.sh", ] -outputs = ["../mobile/openapi/lib/"] +outputs = ["../mobile/generated/openapi/"] run = "bash ./bin/generate-dart-sdk.sh" [tasks.open-api] diff --git a/mobile/.gitignore b/mobile/.gitignore index 64aa4a8dd7..c426d46ec3 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -32,8 +32,15 @@ .pub/ /build/ lib/**/*.drift.dart +lib/**/*.freezed.dart +lib/routing/router.gr.dart test/drift/main/generated/ +# Pigeon related +/lib/platform/*.g.dart +/ios/**/*.g.swift +/android/**/*.g.kt + # Web related lib/generated_plugin_registrant.dart diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index f5ead8de2e..7e314e8d81 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -26,15 +26,51 @@ linter: # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - unawaited_futures: true - use_build_context_synchronously: false + # Formatting + avoid_print: true require_trailing_commas: true unrelated_type_equality_checks: true prefer_const_constructors: true always_use_package_imports: true always_put_control_body_on_new_line: true + unnecessary_null_checks: true + unnecessary_parenthesis: true + prefer_final_locals: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + use_super_parameters: true + directives_ordering: true + no_leading_underscores_for_local_identifiers: true + always_declare_return_types: true + avoid_void_async: true + noop_primitive_operations: true + use_named_constants: true + combinators_ordering: true + avoid_multiple_declarations_per_line: true + unnecessary_breaks: true + + # Correctness + no_adjacent_strings_in_list: true + cancel_subscriptions: true + close_sinks: true + unawaited_futures: true + discarded_futures: true + no_self_assignments: true + throw_in_finally: true + collection_methods_unrelated_type: true + cast_nullable_to_non_nullable: true + + # Known issues + avoid_slow_async_io: true + avoid_type_to_string: true + + # Flutter specific + use_build_context_synchronously: true + sized_box_for_whitespace: true + use_colored_box: true + use_decorated_box: true + avoid_unnecessary_containers: true + use_full_hex_values_for_flutter_colors: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options @@ -46,20 +82,12 @@ analyzer: - lib/**/*.g.dart - lib/**/*.drift.dart - # TODO: Re-enable after upgrading custom_lint - # plugins: - # - custom_lint + # NOTE: We explicitly do not use riverpod_lint as there are analyzer version conflicts between + # our Flutter version and the required old riverpod_lint 2.x errors: unawaited_futures: warning always_put_control_body_on_new_line: warning -custom_lint: - rules: - - avoid_build_context_in_providers: false - - avoid_public_notifier_properties: false - - avoid_manual_providers_as_generated_provider_dependency: false - - unsupported_provider_value: false - dart_code_metrics: rules: - banned-usage: @@ -82,3 +110,17 @@ dart_code_metrics: - prefer-const-border-radius - prefer-declaring-const-constructor - prefer-switch-expression + + # Riverpod + - avoid-calling-notifier-members-inside-build + - avoid-notifier-constructors + - avoid-nullable-async-value-pattern + - avoid-public-notifier-properties + - avoid-ref-inside-state-dispose + - avoid-ref-read-inside-build + - avoid-ref-watch-outside-build + - avoid-unnecessary-consumer-widgets + - dispose-provided-instances + - prefer-immutable-provider-arguments + - use-ref-and-state-synchronously + - use-ref-read-synchronously diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 1b8d2a97fb..5f4de8284a 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -152,12 +152,6 @@ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> - - - { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -/** Generated class from Pigeon that represents data sent in messages. */ -data class BackgroundWorkerSettings ( - val requiresCharging: Boolean, - val minimumDelaySeconds: Long -) - { - companion object { - fun fromList(pigeonVar_list: List): BackgroundWorkerSettings { - val requiresCharging = pigeonVar_list[0] as Boolean - val minimumDelaySeconds = pigeonVar_list[1] as Long - return BackgroundWorkerSettings(requiresCharging, minimumDelaySeconds) - } - } - fun toList(): List { - return listOf( - requiresCharging, - minimumDelaySeconds, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as BackgroundWorkerSettings - return BackgroundWorkerPigeonUtils.deepEquals(this.requiresCharging, other.requiresCharging) && BackgroundWorkerPigeonUtils.deepEquals(this.minimumDelaySeconds, other.minimumDelaySeconds) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.requiresCharging) - result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.minimumDelaySeconds) - return result - } -} -private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - BackgroundWorkerSettings.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is BackgroundWorkerSettings -> { - stream.write(129) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface BackgroundWorkerFgHostApi { - fun enable() - fun saveNotificationMessage(title: String, body: String) - fun configure(settings: BackgroundWorkerSettings) - fun disable() - - companion object { - /** The codec used by BackgroundWorkerFgHostApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerPigeonCodec() - } - /** Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.enable() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val titleArg = args[0] as String - val bodyArg = args[1] as String - val wrapped: List = try { - api.saveNotificationMessage(titleArg, bodyArg) - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val settingsArg = args[0] as BackgroundWorkerSettings - val wrapped: List = try { - api.configure(settingsArg) - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.disable() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface BackgroundWorkerBgHostApi { - fun onInitialized() - fun close() - - companion object { - /** The codec used by BackgroundWorkerBgHostApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerPigeonCodec() - } - /** Sets up an instance of `BackgroundWorkerBgHostApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerBgHostApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.onInitialized() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.close() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} -/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ -class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { - companion object { - /** The codec used by BackgroundWorkerFlutterApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerPigeonCodec() - } - } - fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(isRefreshArg, maxSecondsArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) - } - } - } - fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(maxMinutesArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) - } - } - } - fun cancel(callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(null) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt deleted file mode 100644 index 4e2e382c2b..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt +++ /dev/null @@ -1,95 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.background - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object BackgroundWorkerLockPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} -private open class BackgroundWorkerLockPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) - } -} - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface BackgroundWorkerLockApi { - fun lock() - fun unlock() - - companion object { - /** The codec used by BackgroundWorkerLockApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerLockPigeonCodec() - } - /** Sets up an instance of `BackgroundWorkerLockApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerLockApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.lock() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerLockPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.unlock() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerLockPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt deleted file mode 100644 index aec1f06164..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt +++ /dev/null @@ -1,116 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.connectivity - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object ConnectivityPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -enum class NetworkCapability(val raw: Int) { - CELLULAR(0), - WIFI(1), - VPN(2), - UNMETERED(3); - - companion object { - fun ofRaw(raw: Int): NetworkCapability? { - return values().firstOrNull { it.raw == raw } - } - } -} -private open class ConnectivityPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - NetworkCapability.ofRaw(it.toInt()) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is NetworkCapability -> { - stream.write(129) - writeValue(stream, value.raw.toLong()) - } - else -> super.writeValue(stream, value) - } - } -} - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface ConnectivityApi { - fun getCapabilities(): List - - companion object { - /** The codec used by ConnectivityApi. */ - val codec: MessageCodec by lazy { - ConnectivityPigeonCodec() - } - /** Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val taskQueue = binaryMessenger.makeBackgroundTaskQueue() - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getCapabilities()) - } catch (exception: Throwable) { - ConnectivityPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt deleted file mode 100644 index c380a0a6a5..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt +++ /dev/null @@ -1,451 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.core - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object NetworkPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -/** Generated class from Pigeon that represents data sent in messages. */ -data class ClientCertData ( - val data: ByteArray, - val password: String -) - { - companion object { - fun fromList(pigeonVar_list: List): ClientCertData { - val data = pigeonVar_list[0] as ByteArray - val password = pigeonVar_list[1] as String - return ClientCertData(data, password) - } - } - fun toList(): List { - return listOf( - data, - password, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as ClientCertData - return NetworkPigeonUtils.deepEquals(this.data, other.data) && NetworkPigeonUtils.deepEquals(this.password, other.password) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + NetworkPigeonUtils.deepHash(this.data) - result = 31 * result + NetworkPigeonUtils.deepHash(this.password) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class ClientCertPrompt ( - val title: String, - val message: String, - val cancel: String, - val confirm: String -) - { - companion object { - fun fromList(pigeonVar_list: List): ClientCertPrompt { - val title = pigeonVar_list[0] as String - val message = pigeonVar_list[1] as String - val cancel = pigeonVar_list[2] as String - val confirm = pigeonVar_list[3] as String - return ClientCertPrompt(title, message, cancel, confirm) - } - } - fun toList(): List { - return listOf( - title, - message, - cancel, - confirm, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as ClientCertPrompt - return NetworkPigeonUtils.deepEquals(this.title, other.title) && NetworkPigeonUtils.deepEquals(this.message, other.message) && NetworkPigeonUtils.deepEquals(this.cancel, other.cancel) && NetworkPigeonUtils.deepEquals(this.confirm, other.confirm) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + NetworkPigeonUtils.deepHash(this.title) - result = 31 * result + NetworkPigeonUtils.deepHash(this.message) - result = 31 * result + NetworkPigeonUtils.deepHash(this.cancel) - result = 31 * result + NetworkPigeonUtils.deepHash(this.confirm) - return result - } -} -private open class NetworkPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - ClientCertData.fromList(it) - } - } - 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - ClientCertPrompt.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is ClientCertData -> { - stream.write(129) - writeValue(stream, value.toList()) - } - is ClientCertPrompt -> { - stream.write(130) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface NetworkApi { - fun addCertificate(clientData: ClientCertData, callback: (Result) -> Unit) - fun selectCertificate(promptText: ClientCertPrompt, callback: (Result) -> Unit) - fun removeCertificate(callback: (Result) -> Unit) - fun hasCertificate(): Boolean - fun getClientPointer(): Long - fun setRequestHeaders(headers: Map, serverUrls: List, token: String?) - fun getAppGroupId(): String - - companion object { - /** The codec used by NetworkApi. */ - val codec: MessageCodec by lazy { - NetworkPigeonCodec() - } - /** Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val clientDataArg = args[0] as ClientCertData - api.addCertificate(clientDataArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(NetworkPigeonUtils.wrapError(error)) - } else { - reply.reply(NetworkPigeonUtils.wrapResult(null)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val promptTextArg = args[0] as ClientCertPrompt - api.selectCertificate(promptTextArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(NetworkPigeonUtils.wrapError(error)) - } else { - reply.reply(NetworkPigeonUtils.wrapResult(null)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.removeCertificate{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(NetworkPigeonUtils.wrapError(error)) - } else { - reply.reply(NetworkPigeonUtils.wrapResult(null)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.hasCertificate()) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getClientPointer()) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val headersArg = args[0] as Map - val serverUrlsArg = args[1] as List - val tokenArg = args[2] as String? - val wrapped: List = try { - api.setRequestHeaders(headersArg, serverUrlsArg, tokenArg) - listOf(null) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getAppGroupId()) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt deleted file mode 100644 index e741ce07e9..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt +++ /dev/null @@ -1,140 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.images - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object LocalImagesPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() -private open class LocalImagesPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface LocalImageApi { - fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, preferEncoded: Boolean, callback: (Result?>) -> Unit) - fun cancelRequest(requestId: Long) - fun getThumbhash(thumbhash: String, callback: (Result>) -> Unit) - - companion object { - /** The codec used by LocalImageApi. */ - val codec: MessageCodec by lazy { - LocalImagesPigeonCodec() - } - /** Sets up an instance of `LocalImageApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: LocalImageApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val assetIdArg = args[0] as String - val requestIdArg = args[1] as Long - val widthArg = args[2] as Long - val heightArg = args[3] as Long - val isVideoArg = args[4] as Boolean - val preferEncodedArg = args[5] as Boolean - api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg, preferEncodedArg) { result: Result?> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(LocalImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(LocalImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val requestIdArg = args[0] as Long - val wrapped: List = try { - api.cancelRequest(requestIdArg) - listOf(null) - } catch (exception: Throwable) { - LocalImagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val thumbhashArg = args[0] as String - api.getThumbhash(thumbhashArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(LocalImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(LocalImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt deleted file mode 100644 index 2b5f4d2f57..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt +++ /dev/null @@ -1,123 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.images - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object RemoteImagesPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} -private open class RemoteImagesPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface RemoteImageApi { - fun requestImage(url: String, requestId: Long, preferEncoded: Boolean, callback: (Result?>) -> Unit) - fun cancelRequest(requestId: Long) - fun clearCache(callback: (Result) -> Unit) - - companion object { - /** The codec used by RemoteImageApi. */ - val codec: MessageCodec by lazy { - RemoteImagesPigeonCodec() - } - /** Sets up an instance of `RemoteImageApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: RemoteImageApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val urlArg = args[0] as String - val requestIdArg = args[1] as Long - val preferEncodedArg = args[2] as Boolean - api.requestImage(urlArg, requestIdArg, preferEncodedArg) { result: Result?> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(RemoteImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(RemoteImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val requestIdArg = args[0] as Long - val wrapped: List = try { - api.cancelRequest(requestIdArg) - listOf(null) - } catch (exception: Throwable) { - RemoteImagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.clearCache{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(RemoteImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(RemoteImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt deleted file mode 100644 index 5f7bf806b4..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt +++ /dev/null @@ -1,169 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.permission - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object PermissionApiPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -enum class PermissionStatus(val raw: Int) { - GRANTED(0), - DENIED(1), - PERMANENTLY_DENIED(2); - - companion object { - fun ofRaw(raw: Int): PermissionStatus? { - return values().firstOrNull { it.raw == raw } - } - } -} -private open class PermissionApiPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - PermissionStatus.ofRaw(it.toInt()) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is PermissionStatus -> { - stream.write(129) - writeValue(stream, value.raw.toLong()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface PermissionApi { - fun isIgnoringBatteryOptimizations(): PermissionStatus - fun hasManageMediaPermission(): Boolean - fun requestManageMediaPermission(callback: (Result) -> Unit) - fun manageMediaPermission(callback: (Result) -> Unit) - - companion object { - /** The codec used by PermissionApi. */ - val codec: MessageCodec by lazy { - PermissionApiPigeonCodec() - } - /** Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.isIgnoringBatteryOptimizations()) - } catch (exception: Throwable) { - PermissionApiPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.hasManageMediaPermission()) - } catch (exception: Throwable) { - PermissionApiPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.requestManageMediaPermission{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(PermissionApiPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(PermissionApiPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.manageMediaPermission{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(PermissionApiPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(PermissionApiPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt deleted file mode 100644 index 02f1cb237d..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt +++ /dev/null @@ -1,823 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.sync - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object MessagesPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -enum class PlatformAssetPlaybackStyle(val raw: Int) { - UNKNOWN(0), - IMAGE(1), - VIDEO(2), - IMAGE_ANIMATED(3), - LIVE_PHOTO(4), - VIDEO_LOOPING(5); - - companion object { - fun ofRaw(raw: Int): PlatformAssetPlaybackStyle? { - return values().firstOrNull { it.raw == raw } - } - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class PlatformAsset ( - val id: String, - val name: String, - val type: Long, - val createdAt: Long? = null, - val updatedAt: Long? = null, - val width: Long? = null, - val height: Long? = null, - val durationMs: Long, - val orientation: Long, - val isFavorite: Boolean, - val adjustmentTime: Long? = null, - val latitude: Double? = null, - val longitude: Double? = null, - val playbackStyle: PlatformAssetPlaybackStyle -) - { - companion object { - fun fromList(pigeonVar_list: List): PlatformAsset { - val id = pigeonVar_list[0] as String - val name = pigeonVar_list[1] as String - val type = pigeonVar_list[2] as Long - val createdAt = pigeonVar_list[3] as Long? - val updatedAt = pigeonVar_list[4] as Long? - val width = pigeonVar_list[5] as Long? - val height = pigeonVar_list[6] as Long? - val durationMs = pigeonVar_list[7] as Long - val orientation = pigeonVar_list[8] as Long - val isFavorite = pigeonVar_list[9] as Boolean - val adjustmentTime = pigeonVar_list[10] as Long? - val latitude = pigeonVar_list[11] as Double? - val longitude = pigeonVar_list[12] as Double? - val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle - return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationMs, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle) - } - } - fun toList(): List { - return listOf( - id, - name, - type, - createdAt, - updatedAt, - width, - height, - durationMs, - orientation, - isFavorite, - adjustmentTime, - latitude, - longitude, - playbackStyle, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as PlatformAsset - return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationMs, other.durationMs) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.id) - result = 31 * result + MessagesPigeonUtils.deepHash(this.name) - result = 31 * result + MessagesPigeonUtils.deepHash(this.type) - result = 31 * result + MessagesPigeonUtils.deepHash(this.createdAt) - result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt) - result = 31 * result + MessagesPigeonUtils.deepHash(this.width) - result = 31 * result + MessagesPigeonUtils.deepHash(this.height) - result = 31 * result + MessagesPigeonUtils.deepHash(this.durationMs) - result = 31 * result + MessagesPigeonUtils.deepHash(this.orientation) - result = 31 * result + MessagesPigeonUtils.deepHash(this.isFavorite) - result = 31 * result + MessagesPigeonUtils.deepHash(this.adjustmentTime) - result = 31 * result + MessagesPigeonUtils.deepHash(this.latitude) - result = 31 * result + MessagesPigeonUtils.deepHash(this.longitude) - result = 31 * result + MessagesPigeonUtils.deepHash(this.playbackStyle) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class PlatformAlbum ( - val id: String, - val name: String, - val updatedAt: Long? = null, - val isCloud: Boolean, - val assetCount: Long -) - { - companion object { - fun fromList(pigeonVar_list: List): PlatformAlbum { - val id = pigeonVar_list[0] as String - val name = pigeonVar_list[1] as String - val updatedAt = pigeonVar_list[2] as Long? - val isCloud = pigeonVar_list[3] as Boolean - val assetCount = pigeonVar_list[4] as Long - return PlatformAlbum(id, name, updatedAt, isCloud, assetCount) - } - } - fun toList(): List { - return listOf( - id, - name, - updatedAt, - isCloud, - assetCount, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as PlatformAlbum - return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.isCloud, other.isCloud) && MessagesPigeonUtils.deepEquals(this.assetCount, other.assetCount) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.id) - result = 31 * result + MessagesPigeonUtils.deepHash(this.name) - result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt) - result = 31 * result + MessagesPigeonUtils.deepHash(this.isCloud) - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetCount) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class SyncDelta ( - val hasChanges: Boolean, - val updates: List, - val deletes: List, - val assetAlbums: Map> -) - { - companion object { - fun fromList(pigeonVar_list: List): SyncDelta { - val hasChanges = pigeonVar_list[0] as Boolean - val updates = pigeonVar_list[1] as List - val deletes = pigeonVar_list[2] as List - val assetAlbums = pigeonVar_list[3] as Map> - return SyncDelta(hasChanges, updates, deletes, assetAlbums) - } - } - fun toList(): List { - return listOf( - hasChanges, - updates, - deletes, - assetAlbums, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as SyncDelta - return MessagesPigeonUtils.deepEquals(this.hasChanges, other.hasChanges) && MessagesPigeonUtils.deepEquals(this.updates, other.updates) && MessagesPigeonUtils.deepEquals(this.deletes, other.deletes) && MessagesPigeonUtils.deepEquals(this.assetAlbums, other.assetAlbums) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.hasChanges) - result = 31 * result + MessagesPigeonUtils.deepHash(this.updates) - result = 31 * result + MessagesPigeonUtils.deepHash(this.deletes) - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetAlbums) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class HashResult ( - val assetId: String, - val error: String? = null, - val hash: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): HashResult { - val assetId = pigeonVar_list[0] as String - val error = pigeonVar_list[1] as String? - val hash = pigeonVar_list[2] as String? - return HashResult(assetId, error, hash) - } - } - fun toList(): List { - return listOf( - assetId, - error, - hash, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as HashResult - return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.hash, other.hash) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId) - result = 31 * result + MessagesPigeonUtils.deepHash(this.error) - result = 31 * result + MessagesPigeonUtils.deepHash(this.hash) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class CloudIdResult ( - val assetId: String, - val error: String? = null, - val cloudId: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): CloudIdResult { - val assetId = pigeonVar_list[0] as String - val error = pigeonVar_list[1] as String? - val cloudId = pigeonVar_list[2] as String? - return CloudIdResult(assetId, error, cloudId) - } - } - fun toList(): List { - return listOf( - assetId, - error, - cloudId, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as CloudIdResult - return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.cloudId, other.cloudId) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId) - result = 31 * result + MessagesPigeonUtils.deepHash(this.error) - result = 31 * result + MessagesPigeonUtils.deepHash(this.cloudId) - return result - } -} -private open class MessagesPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - PlatformAssetPlaybackStyle.ofRaw(it.toInt()) - } - } - 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - PlatformAsset.fromList(it) - } - } - 131.toByte() -> { - return (readValue(buffer) as? List)?.let { - PlatformAlbum.fromList(it) - } - } - 132.toByte() -> { - return (readValue(buffer) as? List)?.let { - SyncDelta.fromList(it) - } - } - 133.toByte() -> { - return (readValue(buffer) as? List)?.let { - HashResult.fromList(it) - } - } - 134.toByte() -> { - return (readValue(buffer) as? List)?.let { - CloudIdResult.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is PlatformAssetPlaybackStyle -> { - stream.write(129) - writeValue(stream, value.raw.toLong()) - } - is PlatformAsset -> { - stream.write(130) - writeValue(stream, value.toList()) - } - is PlatformAlbum -> { - stream.write(131) - writeValue(stream, value.toList()) - } - is SyncDelta -> { - stream.write(132) - writeValue(stream, value.toList()) - } - is HashResult -> { - stream.write(133) - writeValue(stream, value.toList()) - } - is CloudIdResult -> { - stream.write(134) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface NativeSyncApi { - fun shouldFullSync(callback: (Result) -> Unit) - fun getMediaChanges(callback: (Result) -> Unit) - fun checkpointSync() - fun clearSyncCheckpoint() - fun getAssetIdsForAlbum(albumId: String, callback: (Result>) -> Unit) - fun getAlbums(callback: (Result>) -> Unit) - fun getAssetsCountSince(albumId: String, timestamp: Long): Long - fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result>) -> Unit) - fun hashAssets(assetIds: List, allowNetworkAccess: Boolean, callback: (Result>) -> Unit) - fun cancelHashing() - fun cancelSync() - fun getTrashedAssets(): Map> - fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result) -> Unit) - fun getCloudIdForAssetIds(assetIds: List): List - - companion object { - /** The codec used by NativeSyncApi. */ - val codec: MessageCodec by lazy { - MessagesPigeonCodec() - } - /** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val taskQueue = binaryMessenger.makeBackgroundTaskQueue() - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.shouldFullSync{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.getMediaChanges{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.checkpointSync() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.clearSyncCheckpoint() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val albumIdArg = args[0] as String - api.getAssetIdsForAlbum(albumIdArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.getAlbums{ result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val albumIdArg = args[0] as String - val timestampArg = args[1] as Long - val wrapped: List = try { - listOf(api.getAssetsCountSince(albumIdArg, timestampArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val albumIdArg = args[0] as String - val updatedTimeCondArg = args[1] as Long? - api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val assetIdsArg = args[0] as List - val allowNetworkAccessArg = args[1] as Boolean - api.hashAssets(assetIdsArg, allowNetworkAccessArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.cancelHashing() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.cancelSync() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getTrashedAssets()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val mediaIdArg = args[0] as String - val typeArg = args[1] as Long - api.restoreFromTrashById(mediaIdArg, typeArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val assetIdsArg = args[0] as List - val wrapped: List = try { - listOf(api.getCloudIdForAssetIds(assetIdsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index 18b771a613..25a849ad3a 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.database.Cursor -import androidx.exifinterface.media.ExifInterface import android.os.Build import android.os.Bundle import android.os.ext.SdkExtensions @@ -30,6 +29,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import java.io.File +import java.io.IOException +import java.io.InputStream import java.security.MessageDigest import kotlin.coroutines.cancellation.CancellationException @@ -377,7 +378,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa )?.use { cursor -> cursor.count.toLong() } ?: 0L - fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result>) -> Unit) { + fun getAssetsForAlbum( + albumId: String, + updatedTimeCond: Long?, + callback: (Result>) -> Unit + ) { runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) } } @@ -419,7 +424,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa }.awaitAll() completeWhenActive(callback, Result.success(results)) - } catch (e: CancellationException) { + } catch (_: CancellationException) { completeWhenActive( callback, Result.failure( FlutterError( @@ -437,23 +442,20 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa private suspend fun hashAsset(assetId: String): HashResult { return try { - val assetUri = ContentUris.withAppendedId( - MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), - assetId.toLong() - ) - val digest = MessageDigest.getInstance("SHA-1") - ctx.contentResolver.openInputStream(assetUri)?.use { inputStream -> - var bytesRead: Int + openOriginalStream(assetId).use { inputStream -> val buffer = ByteArray(HASH_BUFFER_SIZE) - while (inputStream.read(buffer).also { bytesRead = it } > 0) { + while (true) { + val bytesRead = inputStream.read(buffer) + if (bytesRead == -1) break currentCoroutineContext().ensureActive() digest.update(buffer, 0, bytesRead) } - } ?: return HashResult(assetId, "Cannot open input stream for asset", null) + } - val hashString = Base64.encodeToString(digest.digest(), Base64.NO_WRAP) - HashResult(assetId, null, hashString) + HashResult(assetId, null, Base64.encodeToString(digest.digest(), Base64.NO_WRAP)) + } catch (e: CancellationException) { + throw e } catch (e: SecurityException) { HashResult(assetId, "Permission denied accessing asset: ${e.message}", null) } catch (e: Exception) { @@ -461,6 +463,37 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa } } + private fun openOriginalStream(assetId: String): InputStream { + val id = assetId.toLong() + val collection = when (getMediaType(id)) { + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + else -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + val uri = ContentUris.withAppendedId(collection, id) + val original = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStore.setRequireOriginal(uri) + } else { + uri + } + + return ctx.contentResolver.openInputStream(original) + ?: throw IOException("Cannot open original stream for asset $assetId") + } + + private fun getMediaType(id: Long): Int = + getCursor( + MediaStore.VOLUME_EXTERNAL, + "${MediaStore.MediaColumns._ID} = ?", + arrayOf(id.toString()), + arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE) + )?.use { cursor -> + if (cursor.moveToFirst()) { + cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)) + } else { + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + } + } ?: MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + fun cancelHashing() { hashTask?.cancel() hashTask = null @@ -476,8 +509,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa syncJob = CoroutineScope(Dispatchers.IO).launch { try { completeWhenActive(callback, Result.success(work())) - } catch (e: CancellationException) { - completeWhenActive(callback, Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null))) + } catch (_: CancellationException) { + completeWhenActive( + callback, + Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null)) + ) } catch (e: Exception) { completeWhenActive(callback, Result.failure(e)) } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt deleted file mode 100644 index 1d5af15cb4..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt +++ /dev/null @@ -1,292 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.viewintent - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object ViewIntentPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -/** Generated class from Pigeon that represents data sent in messages. */ -data class ViewIntentPayload ( - val path: String? = null, - val mimeType: String, - val localAssetId: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): ViewIntentPayload { - val path = pigeonVar_list[0] as String? - val mimeType = pigeonVar_list[1] as String - val localAssetId = pigeonVar_list[2] as String? - return ViewIntentPayload(path, mimeType, localAssetId) - } - } - fun toList(): List { - return listOf( - path, - mimeType, - localAssetId, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as ViewIntentPayload - return ViewIntentPigeonUtils.deepEquals(this.path, other.path) && ViewIntentPigeonUtils.deepEquals(this.mimeType, other.mimeType) && ViewIntentPigeonUtils.deepEquals(this.localAssetId, other.localAssetId) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + ViewIntentPigeonUtils.deepHash(this.path) - result = 31 * result + ViewIntentPigeonUtils.deepHash(this.mimeType) - result = 31 * result + ViewIntentPigeonUtils.deepHash(this.localAssetId) - return result - } -} -private open class ViewIntentPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - ViewIntentPayload.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is ViewIntentPayload -> { - stream.write(129) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface ViewIntentHostApi { - fun consumeViewIntent(callback: (Result) -> Unit) - - companion object { - /** The codec used by ViewIntentHostApi. */ - val codec: MessageCodec by lazy { - ViewIntentPigeonCodec() - } - /** Sets up an instance of `ViewIntentHostApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: ViewIntentHostApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.consumeViewIntent{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(ViewIntentPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(ViewIntentPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/bin/generate_keys.dart b/mobile/bin/generate_keys.dart index a4cf562bcb..1ce643be12 100644 --- a/mobile/bin/generate_keys.dart +++ b/mobile/bin/generate_keys.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_print +// ignore_for_file: avoid_slow_async_io, avoid_print import 'dart:convert'; import 'dart:io'; diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift deleted file mode 100644 index bd01e953f9..0000000000 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ /dev/null @@ -1,418 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func createConnectionError(withChannelName channelName: String) -> PigeonError { - return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - -private func doubleEqualsBackgroundWorker(_ lhs: Double, _ rhs: Double) -> Bool { - return (lhs.isNaN && rhs.isNaN) || lhs == rhs -} - -private func doubleHashBackgroundWorker(_ value: Double, _ hasher: inout Hasher) { - if value.isNaN { - hasher.combine(0x7FF8000000000000) - } else { - // Normalize -0.0 to 0.0 - hasher.combine(value == 0 ? 0 : value) - } -} - -func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true - - case (nil, _), (_, nil): - return false - - case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: - return true - - case is (Void, Void): - return true - - case (let lhsArray, let rhsArray) as ([Any?], [Any?]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !deepEqualsBackgroundWorker(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsArray, let rhsArray) as ([Double], [Double]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !doubleEqualsBackgroundWorker(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard lhsDictionary.count == rhsDictionary.count else { return false } - for (lhsKey, lhsValue) in lhsDictionary { - var found = false - for (rhsKey, rhsValue) in rhsDictionary { - if deepEqualsBackgroundWorker(lhsKey, rhsKey) { - if deepEqualsBackgroundWorker(lhsValue, rhsValue) { - found = true - break - } else { - return false - } - } - } - if !found { return false } - } - return true - - case (let lhs as Double, let rhs as Double): - return doubleEqualsBackgroundWorker(lhs, rhs) - - case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): - return lhsHashable == rhsHashable - - default: - return false - } -} - -func deepHashBackgroundWorker(value: Any?, hasher: inout Hasher) { - let cleanValue = nilOrValue(value) as Any? - if let cleanValue = cleanValue { - if let doubleValue = cleanValue as? Double { - doubleHashBackgroundWorker(doubleValue, &hasher) - } else if let valueList = cleanValue as? [Any?] { - for item in valueList { - deepHashBackgroundWorker(value: item, hasher: &hasher) - } - } else if let valueList = cleanValue as? [Double] { - for item in valueList { - doubleHashBackgroundWorker(item, &hasher) - } - } else if let valueDict = cleanValue as? [AnyHashable: Any?] { - var result = 0 - for (key, value) in valueDict { - var entryKeyHasher = Hasher() - deepHashBackgroundWorker(value: key, hasher: &entryKeyHasher) - var entryValueHasher = Hasher() - deepHashBackgroundWorker(value: value, hasher: &entryValueHasher) - result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) - } - hasher.combine(result) - } else if let hashableValue = cleanValue as? AnyHashable { - hasher.combine(hashableValue) - } else { - hasher.combine(String(describing: cleanValue)) - } - } else { - hasher.combine(0) - } -} - - -/// Generated class from Pigeon that represents data sent in messages. -struct BackgroundWorkerSettings: Hashable { - var requiresCharging: Bool - var minimumDelaySeconds: Int64 - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> BackgroundWorkerSettings? { - let requiresCharging = pigeonVar_list[0] as! Bool - let minimumDelaySeconds = pigeonVar_list[1] as! Int64 - - return BackgroundWorkerSettings( - requiresCharging: requiresCharging, - minimumDelaySeconds: minimumDelaySeconds - ) - } - func toList() -> [Any?] { - return [ - requiresCharging, - minimumDelaySeconds, - ] - } - static func == (lhs: BackgroundWorkerSettings, rhs: BackgroundWorkerSettings) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsBackgroundWorker(lhs.requiresCharging, rhs.requiresCharging) && deepEqualsBackgroundWorker(lhs.minimumDelaySeconds, rhs.minimumDelaySeconds) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("BackgroundWorkerSettings") - deepHashBackgroundWorker(value: requiresCharging, hasher: &hasher) - deepHashBackgroundWorker(value: minimumDelaySeconds, hasher: &hasher) - } -} - -private class BackgroundWorkerPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - return BackgroundWorkerSettings.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class BackgroundWorkerPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? BackgroundWorkerSettings { - super.writeByte(129) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class BackgroundWorkerPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return BackgroundWorkerPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return BackgroundWorkerPigeonCodecWriter(data: data) - } -} - -class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = BackgroundWorkerPigeonCodec(readerWriter: BackgroundWorkerPigeonCodecReaderWriter()) -} - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol BackgroundWorkerFgHostApi { - func enable() throws - func saveNotificationMessage(title: String, body: String) throws - func configure(settings: BackgroundWorkerSettings) throws - func disable() throws -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class BackgroundWorkerFgHostApiSetup { - static var codec: FlutterStandardMessageCodec { BackgroundWorkerPigeonCodec.shared } - /// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - enableChannel.setMessageHandler { _, reply in - do { - try api.enable() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - enableChannel.setMessageHandler(nil) - } - let saveNotificationMessageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - saveNotificationMessageChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let titleArg = args[0] as! String - let bodyArg = args[1] as! String - do { - try api.saveNotificationMessage(title: titleArg, body: bodyArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - saveNotificationMessageChannel.setMessageHandler(nil) - } - let configureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - configureChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let settingsArg = args[0] as! BackgroundWorkerSettings - do { - try api.configure(settings: settingsArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - configureChannel.setMessageHandler(nil) - } - let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - disableChannel.setMessageHandler { _, reply in - do { - try api.disable() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - disableChannel.setMessageHandler(nil) - } - } -} -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol BackgroundWorkerBgHostApi { - func onInitialized() throws - func close() throws -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class BackgroundWorkerBgHostApiSetup { - static var codec: FlutterStandardMessageCodec { BackgroundWorkerPigeonCodec.shared } - /// Sets up an instance of `BackgroundWorkerBgHostApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerBgHostApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let onInitializedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - onInitializedChannel.setMessageHandler { _, reply in - do { - try api.onInitialized() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - onInitializedChannel.setMessageHandler(nil) - } - let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - closeChannel.setMessageHandler { _, reply in - do { - try api.close() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - closeChannel.setMessageHandler(nil) - } - } -} -/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. -protocol BackgroundWorkerFlutterApiProtocol { - func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) - func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) - func cancel(completion: @escaping (Result) -> Void) -} -class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { - private let binaryMessenger: FlutterBinaryMessenger - private let messageChannelSuffix: String - init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { - self.binaryMessenger = binaryMessenger - self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - } - var codec: BackgroundWorkerPigeonCodec { - return BackgroundWorkerPigeonCodec.shared - } - func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([isRefreshArg, maxSecondsArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) - } - } - } - func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([maxMinutesArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) - } - } - } - func cancel(completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage(nil) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) - } - } - } -} diff --git a/mobile/ios/Runner/Connectivity/Connectivity.g.swift b/mobile/ios/Runner/Connectivity/Connectivity.g.swift deleted file mode 100644 index c7aff63e10..0000000000 --- a/mobile/ios/Runner/Connectivity/Connectivity.g.swift +++ /dev/null @@ -1,129 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -enum NetworkCapability: Int { - case cellular = 0 - case wifi = 1 - case vpn = 2 - case unmetered = 3 -} - -private class ConnectivityPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return NetworkCapability(rawValue: enumResultAsInt) - } - return nil - default: - return super.readValue(ofType: type) - } - } -} - -private class ConnectivityPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? NetworkCapability { - super.writeByte(129) - super.writeValue(value.rawValue) - } else { - super.writeValue(value) - } - } -} - -private class ConnectivityPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return ConnectivityPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return ConnectivityPigeonCodecWriter(data: data) - } -} - -class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter()) -} - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol ConnectivityApi { - func getCapabilities() throws -> [NetworkCapability] -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class ConnectivityApiSetup { - static var codec: FlutterStandardMessageCodec { ConnectivityPigeonCodec.shared } - /// Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - #if os(iOS) - let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() - #else - let taskQueue: FlutterTaskQueue? = nil - #endif - let getCapabilitiesChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getCapabilitiesChannel.setMessageHandler { _, reply in - do { - let result = try api.getCapabilities() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getCapabilitiesChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift deleted file mode 100644 index 265923d165..0000000000 --- a/mobile/ios/Runner/Core/Network.g.swift +++ /dev/null @@ -1,406 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - -private func doubleEqualsNetwork(_ lhs: Double, _ rhs: Double) -> Bool { - return (lhs.isNaN && rhs.isNaN) || lhs == rhs -} - -private func doubleHashNetwork(_ value: Double, _ hasher: inout Hasher) { - if value.isNaN { - hasher.combine(0x7FF8000000000000) - } else { - // Normalize -0.0 to 0.0 - hasher.combine(value == 0 ? 0 : value) - } -} - -func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true - - case (nil, _), (_, nil): - return false - - case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: - return true - - case is (Void, Void): - return true - - case (let lhsArray, let rhsArray) as ([Any?], [Any?]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !deepEqualsNetwork(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsArray, let rhsArray) as ([Double], [Double]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !doubleEqualsNetwork(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard lhsDictionary.count == rhsDictionary.count else { return false } - for (lhsKey, lhsValue) in lhsDictionary { - var found = false - for (rhsKey, rhsValue) in rhsDictionary { - if deepEqualsNetwork(lhsKey, rhsKey) { - if deepEqualsNetwork(lhsValue, rhsValue) { - found = true - break - } else { - return false - } - } - } - if !found { return false } - } - return true - - case (let lhs as Double, let rhs as Double): - return doubleEqualsNetwork(lhs, rhs) - - case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): - return lhsHashable == rhsHashable - - default: - return false - } -} - -func deepHashNetwork(value: Any?, hasher: inout Hasher) { - let cleanValue = nilOrValue(value) as Any? - if let cleanValue = cleanValue { - if let doubleValue = cleanValue as? Double { - doubleHashNetwork(doubleValue, &hasher) - } else if let valueList = cleanValue as? [Any?] { - for item in valueList { - deepHashNetwork(value: item, hasher: &hasher) - } - } else if let valueList = cleanValue as? [Double] { - for item in valueList { - doubleHashNetwork(item, &hasher) - } - } else if let valueDict = cleanValue as? [AnyHashable: Any?] { - var result = 0 - for (key, value) in valueDict { - var entryKeyHasher = Hasher() - deepHashNetwork(value: key, hasher: &entryKeyHasher) - var entryValueHasher = Hasher() - deepHashNetwork(value: value, hasher: &entryValueHasher) - result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) - } - hasher.combine(result) - } else if let hashableValue = cleanValue as? AnyHashable { - hasher.combine(hashableValue) - } else { - hasher.combine(String(describing: cleanValue)) - } - } else { - hasher.combine(0) - } -} - - -/// Generated class from Pigeon that represents data sent in messages. -struct ClientCertData: Hashable { - var data: FlutterStandardTypedData - var password: String - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertData? { - let data = pigeonVar_list[0] as! FlutterStandardTypedData - let password = pigeonVar_list[1] as! String - - return ClientCertData( - data: data, - password: password - ) - } - func toList() -> [Any?] { - return [ - data, - password, - ] - } - static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsNetwork(lhs.data, rhs.data) && deepEqualsNetwork(lhs.password, rhs.password) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("ClientCertData") - deepHashNetwork(value: data, hasher: &hasher) - deepHashNetwork(value: password, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct ClientCertPrompt: Hashable { - var title: String - var message: String - var cancel: String - var confirm: String - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertPrompt? { - let title = pigeonVar_list[0] as! String - let message = pigeonVar_list[1] as! String - let cancel = pigeonVar_list[2] as! String - let confirm = pigeonVar_list[3] as! String - - return ClientCertPrompt( - title: title, - message: message, - cancel: cancel, - confirm: confirm - ) - } - func toList() -> [Any?] { - return [ - title, - message, - cancel, - confirm, - ] - } - static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsNetwork(lhs.title, rhs.title) && deepEqualsNetwork(lhs.message, rhs.message) && deepEqualsNetwork(lhs.cancel, rhs.cancel) && deepEqualsNetwork(lhs.confirm, rhs.confirm) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("ClientCertPrompt") - deepHashNetwork(value: title, hasher: &hasher) - deepHashNetwork(value: message, hasher: &hasher) - deepHashNetwork(value: cancel, hasher: &hasher) - deepHashNetwork(value: confirm, hasher: &hasher) - } -} - -private class NetworkPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - return ClientCertData.fromList(self.readValue() as! [Any?]) - case 130: - return ClientCertPrompt.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class NetworkPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? ClientCertData { - super.writeByte(129) - super.writeValue(value.toList()) - } else if let value = value as? ClientCertPrompt { - super.writeByte(130) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class NetworkPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return NetworkPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return NetworkPigeonCodecWriter(data: data) - } -} - -class NetworkPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = NetworkPigeonCodec(readerWriter: NetworkPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol NetworkApi { - func addCertificate(clientData: ClientCertData, completion: @escaping (Result) -> Void) - func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) - func removeCertificate(completion: @escaping (Result) -> Void) - func hasCertificate() throws -> Bool - func getClientPointer() throws -> Int64 - func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws - func getAppGroupId() throws -> String -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class NetworkApiSetup { - static var codec: FlutterStandardMessageCodec { NetworkPigeonCodec.shared } - /// Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let addCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - addCertificateChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let clientDataArg = args[0] as! ClientCertData - api.addCertificate(clientData: clientDataArg) { result in - switch result { - case .success: - reply(wrapResult(nil)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - addCertificateChannel.setMessageHandler(nil) - } - let selectCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - selectCertificateChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let promptTextArg = args[0] as! ClientCertPrompt - api.selectCertificate(promptText: promptTextArg) { result in - switch result { - case .success: - reply(wrapResult(nil)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - selectCertificateChannel.setMessageHandler(nil) - } - let removeCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - removeCertificateChannel.setMessageHandler { _, reply in - api.removeCertificate { result in - switch result { - case .success: - reply(wrapResult(nil)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - removeCertificateChannel.setMessageHandler(nil) - } - let hasCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - hasCertificateChannel.setMessageHandler { _, reply in - do { - let result = try api.hasCertificate() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - hasCertificateChannel.setMessageHandler(nil) - } - let getClientPointerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getClientPointerChannel.setMessageHandler { _, reply in - do { - let result = try api.getClientPointer() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getClientPointerChannel.setMessageHandler(nil) - } - let setRequestHeadersChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - setRequestHeadersChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let headersArg = args[0] as! [String: String] - let serverUrlsArg = args[1] as! [String] - let tokenArg: String? = nilOrValue(args[2]) - do { - try api.setRequestHeaders(headers: headersArg, serverUrls: serverUrlsArg, token: tokenArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - setRequestHeadersChannel.setMessageHandler(nil) - } - let getAppGroupIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAppGroupIdChannel.setMessageHandler { _, reply in - do { - let result = try api.getAppGroupId() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getAppGroupIdChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Images/LocalImages.g.swift b/mobile/ios/Runner/Images/LocalImages.g.swift deleted file mode 100644 index b9324260be..0000000000 --- a/mobile/ios/Runner/Images/LocalImages.g.swift +++ /dev/null @@ -1,139 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -private class LocalImagesPigeonCodecReader: FlutterStandardReader { -} - -private class LocalImagesPigeonCodecWriter: FlutterStandardWriter { -} - -private class LocalImagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return LocalImagesPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return LocalImagesPigeonCodecWriter(data: data) - } -} - -class LocalImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = LocalImagesPigeonCodec(readerWriter: LocalImagesPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol LocalImageApi { - func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void) - func cancelRequest(requestId: Int64) throws - func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void) -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class LocalImageApiSetup { - static var codec: FlutterStandardMessageCodec { LocalImagesPigeonCodec.shared } - /// Sets up an instance of `LocalImageApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: LocalImageApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - requestImageChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let assetIdArg = args[0] as! String - let requestIdArg = args[1] as! Int64 - let widthArg = args[2] as! Int64 - let heightArg = args[3] as! Int64 - let isVideoArg = args[4] as! Bool - let preferEncodedArg = args[5] as! Bool - api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg, preferEncoded: preferEncodedArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - requestImageChannel.setMessageHandler(nil) - } - let cancelRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelRequestChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let requestIdArg = args[0] as! Int64 - do { - try api.cancelRequest(requestId: requestIdArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelRequestChannel.setMessageHandler(nil) - } - let getThumbhashChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getThumbhashChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let thumbhashArg = args[0] as! String - api.getThumbhash(thumbhash: thumbhashArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getThumbhashChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Images/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift index 9c142da054..0d023ad466 100644 --- a/mobile/ios/Runner/Images/LocalImagesImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -20,6 +20,7 @@ class LocalImageApiImpl: LocalImageApi { requestOptions.version = .current return requestOptions }() + private static let maxPixelSize: CGFloat = 16384 private static let registry = RequestRegistry() @@ -108,11 +109,16 @@ class LocalImageApiImpl: LocalImageApi { ])) } + let isOriginal = !(width > 0 && height > 0) + let targetSize = isOriginal + ? CGSize(width: Self.maxPixelSize, height: Self.maxPixelSize) + : CGSize(width: Double(width), height: Double(height)) + let contentMode: PHImageContentMode = isOriginal ? .aspectFit : .aspectFill var image: UIImage? Self.imageManager.requestImage( for: asset, - targetSize: width > 0 && height > 0 ? CGSize(width: Double(width), height: Double(height)) : PHImageManagerMaximumSize, - contentMode: .aspectFill, + targetSize: targetSize, + contentMode: contentMode, options: Self.requestOptions, resultHandler: { (_image, info) -> Void in image = _image @@ -123,12 +129,38 @@ class LocalImageApiImpl: LocalImageApi { return request.completion(ImageProcessing.cancelledResult) } - guard let image = image, - let cgImage = image.cgImage else { + guard let fastImage = image, + var cgImage = fastImage.cgImage else { Self.registry.remove(requestId: requestId) return request.completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil))) } + // .fast can return larger than the target, so retry with .exact to guarantee the bound. + if max(cgImage.width, cgImage.height) > Int(Self.maxPixelSize) { + let exactOptions = Self.requestOptions.copy() as! PHImageRequestOptions + exactOptions.resizeMode = .exact + image = nil + Self.imageManager.requestImage( + for: asset, + targetSize: targetSize, + contentMode: contentMode, + options: exactOptions, + resultHandler: { (_image, info) -> Void in + image = _image + } + ) + + if request.isCancelled { + return request.completion(ImageProcessing.cancelledResult) + } + + guard let exactImage = image?.cgImage else { + Self.registry.remove(requestId: requestId) + return request.completion(.failure(PigeonError(code: "", message: "Could not resize image for \(assetId)", details: nil))) + } + cgImage = exactImage + } + if request.isCancelled { return request.completion(ImageProcessing.cancelledResult) } diff --git a/mobile/ios/Runner/Images/RemoteImages.g.swift b/mobile/ios/Runner/Images/RemoteImages.g.swift deleted file mode 100644 index 12eaaeec60..0000000000 --- a/mobile/ios/Runner/Images/RemoteImages.g.swift +++ /dev/null @@ -1,134 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -private class RemoteImagesPigeonCodecReader: FlutterStandardReader { -} - -private class RemoteImagesPigeonCodecWriter: FlutterStandardWriter { -} - -private class RemoteImagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return RemoteImagesPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return RemoteImagesPigeonCodecWriter(data: data) - } -} - -class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = RemoteImagesPigeonCodec(readerWriter: RemoteImagesPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol RemoteImageApi { - func requestImage(url: String, requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void) - func cancelRequest(requestId: Int64) throws - func clearCache(completion: @escaping (Result) -> Void) -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class RemoteImageApiSetup { - static var codec: FlutterStandardMessageCodec { RemoteImagesPigeonCodec.shared } - /// Sets up an instance of `RemoteImageApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: RemoteImageApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - requestImageChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let urlArg = args[0] as! String - let requestIdArg = args[1] as! Int64 - let preferEncodedArg = args[2] as! Bool - api.requestImage(url: urlArg, requestId: requestIdArg, preferEncoded: preferEncodedArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - requestImageChannel.setMessageHandler(nil) - } - let cancelRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelRequestChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let requestIdArg = args[0] as! Int64 - do { - try api.cancelRequest(requestId: requestIdArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelRequestChannel.setMessageHandler(nil) - } - let clearCacheChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - clearCacheChannel.setMessageHandler { _, reply in - api.clearCache { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - clearCacheChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Permission/PermissionApi.g.swift b/mobile/ios/Runner/Permission/PermissionApi.g.swift deleted file mode 100644 index b9c116f0c5..0000000000 --- a/mobile/ios/Runner/Permission/PermissionApi.g.swift +++ /dev/null @@ -1,168 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -enum PermissionStatus: Int { - case granted = 0 - case denied = 1 - case permanentlyDenied = 2 -} - -private class PermissionApiPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return PermissionStatus(rawValue: enumResultAsInt) - } - return nil - default: - return super.readValue(ofType: type) - } - } -} - -private class PermissionApiPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? PermissionStatus { - super.writeByte(129) - super.writeValue(value.rawValue) - } else { - super.writeValue(value) - } - } -} - -private class PermissionApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return PermissionApiPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return PermissionApiPigeonCodecWriter(data: data) - } -} - -class PermissionApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = PermissionApiPigeonCodec(readerWriter: PermissionApiPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol PermissionApi { - func isIgnoringBatteryOptimizations() throws -> PermissionStatus - func hasManageMediaPermission() throws -> Bool - func requestManageMediaPermission(completion: @escaping (Result) -> Void) - func manageMediaPermission(completion: @escaping (Result) -> Void) -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class PermissionApiSetup { - static var codec: FlutterStandardMessageCodec { PermissionApiPigeonCodec.shared } - /// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let isIgnoringBatteryOptimizationsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - isIgnoringBatteryOptimizationsChannel.setMessageHandler { _, reply in - do { - let result = try api.isIgnoringBatteryOptimizations() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - isIgnoringBatteryOptimizationsChannel.setMessageHandler(nil) - } - let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - hasManageMediaPermissionChannel.setMessageHandler { _, reply in - do { - let result = try api.hasManageMediaPermission() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - hasManageMediaPermissionChannel.setMessageHandler(nil) - } - let requestManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - requestManageMediaPermissionChannel.setMessageHandler { _, reply in - api.requestManageMediaPermission { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - requestManageMediaPermissionChannel.setMessageHandler(nil) - } - let manageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - manageMediaPermissionChannel.setMessageHandler { _, reply in - api.manageMediaPermission { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - manageMediaPermissionChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift deleted file mode 100644 index a752785c5b..0000000000 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ /dev/null @@ -1,777 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -/// Error class for passing custom error details to Dart side. -final class PigeonError: Error { - let code: String - let message: String? - let details: Sendable? - - init(code: String, message: String?, details: Sendable?) { - self.code = code - self.message = message - self.details = details - } - - var localizedDescription: String { - return - "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" - } -} - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - -private func doubleEqualsMessages(_ lhs: Double, _ rhs: Double) -> Bool { - return (lhs.isNaN && rhs.isNaN) || lhs == rhs -} - -private func doubleHashMessages(_ value: Double, _ hasher: inout Hasher) { - if value.isNaN { - hasher.combine(0x7FF8000000000000) - } else { - // Normalize -0.0 to 0.0 - hasher.combine(value == 0 ? 0 : value) - } -} - -func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true - - case (nil, _), (_, nil): - return false - - case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: - return true - - case is (Void, Void): - return true - - case (let lhsArray, let rhsArray) as ([Any?], [Any?]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !deepEqualsMessages(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsArray, let rhsArray) as ([Double], [Double]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !doubleEqualsMessages(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard lhsDictionary.count == rhsDictionary.count else { return false } - for (lhsKey, lhsValue) in lhsDictionary { - var found = false - for (rhsKey, rhsValue) in rhsDictionary { - if deepEqualsMessages(lhsKey, rhsKey) { - if deepEqualsMessages(lhsValue, rhsValue) { - found = true - break - } else { - return false - } - } - } - if !found { return false } - } - return true - - case (let lhs as Double, let rhs as Double): - return doubleEqualsMessages(lhs, rhs) - - case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): - return lhsHashable == rhsHashable - - default: - return false - } -} - -func deepHashMessages(value: Any?, hasher: inout Hasher) { - let cleanValue = nilOrValue(value) as Any? - if let cleanValue = cleanValue { - if let doubleValue = cleanValue as? Double { - doubleHashMessages(doubleValue, &hasher) - } else if let valueList = cleanValue as? [Any?] { - for item in valueList { - deepHashMessages(value: item, hasher: &hasher) - } - } else if let valueList = cleanValue as? [Double] { - for item in valueList { - doubleHashMessages(item, &hasher) - } - } else if let valueDict = cleanValue as? [AnyHashable: Any?] { - var result = 0 - for (key, value) in valueDict { - var entryKeyHasher = Hasher() - deepHashMessages(value: key, hasher: &entryKeyHasher) - var entryValueHasher = Hasher() - deepHashMessages(value: value, hasher: &entryValueHasher) - result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) - } - hasher.combine(result) - } else if let hashableValue = cleanValue as? AnyHashable { - hasher.combine(hashableValue) - } else { - hasher.combine(String(describing: cleanValue)) - } - } else { - hasher.combine(0) - } -} - - -enum PlatformAssetPlaybackStyle: Int { - case unknown = 0 - case image = 1 - case video = 2 - case imageAnimated = 3 - case livePhoto = 4 - case videoLooping = 5 -} - -/// Generated class from Pigeon that represents data sent in messages. -struct PlatformAsset: Hashable { - var id: String - var name: String - var type: Int64 - var createdAt: Int64? = nil - var updatedAt: Int64? = nil - var width: Int64? = nil - var height: Int64? = nil - var durationMs: Int64 - var orientation: Int64 - var isFavorite: Bool - var adjustmentTime: Int64? = nil - var latitude: Double? = nil - var longitude: Double? = nil - var playbackStyle: PlatformAssetPlaybackStyle - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? { - let id = pigeonVar_list[0] as! String - let name = pigeonVar_list[1] as! String - let type = pigeonVar_list[2] as! Int64 - let createdAt: Int64? = nilOrValue(pigeonVar_list[3]) - let updatedAt: Int64? = nilOrValue(pigeonVar_list[4]) - let width: Int64? = nilOrValue(pigeonVar_list[5]) - let height: Int64? = nilOrValue(pigeonVar_list[6]) - let durationMs = pigeonVar_list[7] as! Int64 - let orientation = pigeonVar_list[8] as! Int64 - let isFavorite = pigeonVar_list[9] as! Bool - let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10]) - let latitude: Double? = nilOrValue(pigeonVar_list[11]) - let longitude: Double? = nilOrValue(pigeonVar_list[12]) - let playbackStyle = pigeonVar_list[13] as! PlatformAssetPlaybackStyle - - return PlatformAsset( - id: id, - name: name, - type: type, - createdAt: createdAt, - updatedAt: updatedAt, - width: width, - height: height, - durationMs: durationMs, - orientation: orientation, - isFavorite: isFavorite, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - playbackStyle: playbackStyle - ) - } - func toList() -> [Any?] { - return [ - id, - name, - type, - createdAt, - updatedAt, - width, - height, - durationMs, - orientation, - isFavorite, - adjustmentTime, - latitude, - longitude, - playbackStyle, - ] - } - static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationMs, rhs.durationMs) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("PlatformAsset") - deepHashMessages(value: id, hasher: &hasher) - deepHashMessages(value: name, hasher: &hasher) - deepHashMessages(value: type, hasher: &hasher) - deepHashMessages(value: createdAt, hasher: &hasher) - deepHashMessages(value: updatedAt, hasher: &hasher) - deepHashMessages(value: width, hasher: &hasher) - deepHashMessages(value: height, hasher: &hasher) - deepHashMessages(value: durationMs, hasher: &hasher) - deepHashMessages(value: orientation, hasher: &hasher) - deepHashMessages(value: isFavorite, hasher: &hasher) - deepHashMessages(value: adjustmentTime, hasher: &hasher) - deepHashMessages(value: latitude, hasher: &hasher) - deepHashMessages(value: longitude, hasher: &hasher) - deepHashMessages(value: playbackStyle, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct PlatformAlbum: Hashable { - var id: String - var name: String - var updatedAt: Int64? = nil - var isCloud: Bool - var assetCount: Int64 - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? { - let id = pigeonVar_list[0] as! String - let name = pigeonVar_list[1] as! String - let updatedAt: Int64? = nilOrValue(pigeonVar_list[2]) - let isCloud = pigeonVar_list[3] as! Bool - let assetCount = pigeonVar_list[4] as! Int64 - - return PlatformAlbum( - id: id, - name: name, - updatedAt: updatedAt, - isCloud: isCloud, - assetCount: assetCount - ) - } - func toList() -> [Any?] { - return [ - id, - name, - updatedAt, - isCloud, - assetCount, - ] - } - static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.isCloud, rhs.isCloud) && deepEqualsMessages(lhs.assetCount, rhs.assetCount) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("PlatformAlbum") - deepHashMessages(value: id, hasher: &hasher) - deepHashMessages(value: name, hasher: &hasher) - deepHashMessages(value: updatedAt, hasher: &hasher) - deepHashMessages(value: isCloud, hasher: &hasher) - deepHashMessages(value: assetCount, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct SyncDelta: Hashable { - var hasChanges: Bool - var updates: [PlatformAsset] - var deletes: [String] - var assetAlbums: [String: [String]] - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? { - let hasChanges = pigeonVar_list[0] as! Bool - let updates = pigeonVar_list[1] as! [PlatformAsset] - let deletes = pigeonVar_list[2] as! [String] - let assetAlbums = pigeonVar_list[3] as! [String: [String]] - - return SyncDelta( - hasChanges: hasChanges, - updates: updates, - deletes: deletes, - assetAlbums: assetAlbums - ) - } - func toList() -> [Any?] { - return [ - hasChanges, - updates, - deletes, - assetAlbums, - ] - } - static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.hasChanges, rhs.hasChanges) && deepEqualsMessages(lhs.updates, rhs.updates) && deepEqualsMessages(lhs.deletes, rhs.deletes) && deepEqualsMessages(lhs.assetAlbums, rhs.assetAlbums) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("SyncDelta") - deepHashMessages(value: hasChanges, hasher: &hasher) - deepHashMessages(value: updates, hasher: &hasher) - deepHashMessages(value: deletes, hasher: &hasher) - deepHashMessages(value: assetAlbums, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct HashResult: Hashable { - var assetId: String - var error: String? = nil - var hash: String? = nil - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> HashResult? { - let assetId = pigeonVar_list[0] as! String - let error: String? = nilOrValue(pigeonVar_list[1]) - let hash: String? = nilOrValue(pigeonVar_list[2]) - - return HashResult( - assetId: assetId, - error: error, - hash: hash - ) - } - func toList() -> [Any?] { - return [ - assetId, - error, - hash, - ] - } - static func == (lhs: HashResult, rhs: HashResult) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.hash, rhs.hash) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("HashResult") - deepHashMessages(value: assetId, hasher: &hasher) - deepHashMessages(value: error, hasher: &hasher) - deepHashMessages(value: hash, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct CloudIdResult: Hashable { - var assetId: String - var error: String? = nil - var cloudId: String? = nil - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> CloudIdResult? { - let assetId = pigeonVar_list[0] as! String - let error: String? = nilOrValue(pigeonVar_list[1]) - let cloudId: String? = nilOrValue(pigeonVar_list[2]) - - return CloudIdResult( - assetId: assetId, - error: error, - cloudId: cloudId - ) - } - func toList() -> [Any?] { - return [ - assetId, - error, - cloudId, - ] - } - static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.cloudId, rhs.cloudId) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("CloudIdResult") - deepHashMessages(value: assetId, hasher: &hasher) - deepHashMessages(value: error, hasher: &hasher) - deepHashMessages(value: cloudId, hasher: &hasher) - } -} - -private class MessagesPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return PlatformAssetPlaybackStyle(rawValue: enumResultAsInt) - } - return nil - case 130: - return PlatformAsset.fromList(self.readValue() as! [Any?]) - case 131: - return PlatformAlbum.fromList(self.readValue() as! [Any?]) - case 132: - return SyncDelta.fromList(self.readValue() as! [Any?]) - case 133: - return HashResult.fromList(self.readValue() as! [Any?]) - case 134: - return CloudIdResult.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class MessagesPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? PlatformAssetPlaybackStyle { - super.writeByte(129) - super.writeValue(value.rawValue) - } else if let value = value as? PlatformAsset { - super.writeByte(130) - super.writeValue(value.toList()) - } else if let value = value as? PlatformAlbum { - super.writeByte(131) - super.writeValue(value.toList()) - } else if let value = value as? SyncDelta { - super.writeByte(132) - super.writeValue(value.toList()) - } else if let value = value as? HashResult { - super.writeByte(133) - super.writeValue(value.toList()) - } else if let value = value as? CloudIdResult { - super.writeByte(134) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return MessagesPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return MessagesPigeonCodecWriter(data: data) - } -} - -class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol NativeSyncApi { - func shouldFullSync(completion: @escaping (Result) -> Void) - func getMediaChanges(completion: @escaping (Result) -> Void) - func checkpointSync() throws - func clearSyncCheckpoint() throws - func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void) - func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void) - func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 - func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void) - func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) - func cancelHashing() throws - func cancelSync() throws - func getTrashedAssets() throws -> [String: [PlatformAsset]] - func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result) -> Void) - func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult] -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class NativeSyncApiSetup { - static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } - /// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - #if os(iOS) - let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() - #else - let taskQueue: FlutterTaskQueue? = nil - #endif - let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - shouldFullSyncChannel.setMessageHandler { _, reply in - api.shouldFullSync { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - shouldFullSyncChannel.setMessageHandler(nil) - } - let getMediaChangesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getMediaChangesChannel.setMessageHandler { _, reply in - api.getMediaChanges { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getMediaChangesChannel.setMessageHandler(nil) - } - let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - checkpointSyncChannel.setMessageHandler { _, reply in - do { - try api.checkpointSync() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - checkpointSyncChannel.setMessageHandler(nil) - } - let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - clearSyncCheckpointChannel.setMessageHandler { _, reply in - do { - try api.clearSyncCheckpoint() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - clearSyncCheckpointChannel.setMessageHandler(nil) - } - let getAssetIdsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAssetIdsForAlbumChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let albumIdArg = args[0] as! String - api.getAssetIdsForAlbum(albumId: albumIdArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getAssetIdsForAlbumChannel.setMessageHandler(nil) - } - let getAlbumsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAlbumsChannel.setMessageHandler { _, reply in - api.getAlbums { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getAlbumsChannel.setMessageHandler(nil) - } - let getAssetsCountSinceChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getAssetsCountSinceChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let albumIdArg = args[0] as! String - let timestampArg = args[1] as! Int64 - do { - let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getAssetsCountSinceChannel.setMessageHandler(nil) - } - let getAssetsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAssetsForAlbumChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let albumIdArg = args[0] as! String - let updatedTimeCondArg: Int64? = nilOrValue(args[1]) - api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getAssetsForAlbumChannel.setMessageHandler(nil) - } - let hashAssetsChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - hashAssetsChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let assetIdsArg = args[0] as! [String] - let allowNetworkAccessArg = args[1] as! Bool - api.hashAssets(assetIds: assetIdsArg, allowNetworkAccess: allowNetworkAccessArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - hashAssetsChannel.setMessageHandler(nil) - } - let cancelHashingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelHashingChannel.setMessageHandler { _, reply in - do { - try api.cancelHashing() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelHashingChannel.setMessageHandler(nil) - } - let cancelSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelSyncChannel.setMessageHandler { _, reply in - do { - try api.cancelSync() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelSyncChannel.setMessageHandler(nil) - } - let getTrashedAssetsChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getTrashedAssetsChannel.setMessageHandler { _, reply in - do { - let result = try api.getTrashedAssets() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getTrashedAssetsChannel.setMessageHandler(nil) - } - let restoreFromTrashByIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - restoreFromTrashByIdChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let mediaIdArg = args[0] as! String - let typeArg = args[1] as! Int64 - api.restoreFromTrashById(mediaId: mediaIdArg, type: typeArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - restoreFromTrashByIdChannel.setMessageHandler(nil) - } - let getCloudIdForAssetIdsChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getCloudIdForAssetIdsChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let assetIdsArg = args[0] as! [String] - do { - let result = try api.getCloudIdForAssetIds(assetIds: assetIdsArg) - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getCloudIdForAssetIdsChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/WidgetExtension/ImageEntry.swift b/mobile/ios/WidgetExtension/ImageEntry.swift index ee371703a8..58f89339ac 100644 --- a/mobile/ios/WidgetExtension/ImageEntry.swift +++ b/mobile/ios/WidgetExtension/ImageEntry.swift @@ -12,13 +12,15 @@ struct ImageEntry: TimelineEntry { var subtitle: String? = nil var error: WidgetError? = nil var deepLink: URL? = nil + var forceFullColor: Bool = true } static func build( api: ImmichAPI, asset: Asset, dateOffset: Int, - subtitle: String? = nil + subtitle: String? = nil, + forceFullColor: Bool = true ) async throws -> Self { @@ -34,7 +36,8 @@ struct ImageEntry: TimelineEntry { image: image, metadata: EntryMetadata( subtitle: subtitle, - deepLink: asset.deepLink + deepLink: asset.deepLink, + forceFullColor: forceFullColor ) ) } @@ -111,12 +114,26 @@ struct ImageEntry: TimelineEntry { } +// Required so that we can decode older versions of the metadata +extension ImageEntry.Metadata { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle) + error = try container.decodeIfPresent(WidgetError.self, forKey: .error) + deepLink = try container.decodeIfPresent(URL.self, forKey: .deepLink) + + forceFullColor = + try container.decodeIfPresent(Bool.self, forKey: .forceFullColor) ?? true + } +} + func generateRandomEntries( api: ImmichAPI, now: Date, count: Int, filter: SearchFilter = Album.NONE.filter, - subtitle: String? = nil + subtitle: String? = nil, + forceFullColor: Bool = true ) async throws -> [ImageEntry] { @@ -132,7 +149,8 @@ func generateRandomEntries( api: api, asset: asset, dateOffset: dateOffset, - subtitle: subtitle + subtitle: subtitle, + forceFullColor: forceFullColor ) } } diff --git a/mobile/ios/WidgetExtension/ImageWidgetView.swift b/mobile/ios/WidgetExtension/ImageWidgetView.swift index 17088d43b8..2e3a816d92 100644 --- a/mobile/ios/WidgetExtension/ImageWidgetView.swift +++ b/mobile/ios/WidgetExtension/ImageWidgetView.swift @@ -3,10 +3,10 @@ import WidgetKit extension Image { @ViewBuilder - func tintedWidgetImageModifier() -> some View { + func tintedWidgetImageModifier(forceFullColor: Bool) -> some View { if #available(iOS 18.0, *) { self - .widgetAccentedRenderingMode(.accentedDesaturated) + .widgetAccentedRenderingMode(forceFullColor ? .accentedDesaturated : .fullColor) } else { self } @@ -18,15 +18,24 @@ struct ImmichWidgetView: View { var body: some View { if let image = entry.image { - ImmichWidgetContentView(image: image, subtitle: entry.metadata.subtitle, deepLink: entry.metadata.deepLink) + ImmichWidgetContentView( + image: image, + subtitle: entry.metadata.subtitle, + deepLink: entry.metadata.deepLink, + forceFullColor: entry.metadata.forceFullColor + ) } else { - ImmichWidgetLoadingView(message: entry.metadata.error?.errorDescription) + ImmichWidgetLoadingView( + message: entry.metadata.error?.errorDescription, + forceFullColor: entry.metadata.forceFullColor + ) } } } private struct ImmichWidgetLoadingView: View { let message: String? + let forceFullColor: Bool var body: some View { let messageText = Text(message ?? "") @@ -39,7 +48,7 @@ private struct ImmichWidgetLoadingView: View { messageText.hidden() Image("LaunchImage") - .tintedWidgetImageModifier() + .tintedWidgetImageModifier(forceFullColor: forceFullColor) messageText } @@ -50,13 +59,14 @@ private struct ImmichWidgetContentView: View { let image: UIImage let subtitle: String? let deepLink: URL? + let forceFullColor: Bool var body: some View { ZStack(alignment: .leading) { Color.clear.overlay( Image(uiImage: image) .resizable() - .tintedWidgetImageModifier() + .tintedWidgetImageModifier(forceFullColor: forceFullColor) .scaledToFill() ) diff --git a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift index 15fa3d722e..dee5a32159 100644 --- a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift +++ b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift @@ -2,7 +2,21 @@ import AppIntents import SwiftUI import WidgetKit -struct ImmichMemoryProvider: TimelineProvider { +// MARK: Widget Configuration + +struct MemoryConfigurationAppIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource { "Memories" } + static var description: IntentDescription { + "See memories from Immich." + } + + @Parameter(title: "Always Display in Full Color", default: false) + var forceFullColor: Bool +} + +// MARK: Provider + +struct ImmichMemoryProvider: AppIntentTimelineProvider { func getYearDifferenceSubtitle(assetYear: Int) -> String { let currentYear = Calendar.current.component(.year, from: Date.now) // construct a "X years ago" subtitle @@ -15,144 +29,132 @@ struct ImmichMemoryProvider: TimelineProvider { ImageEntry(date: Date(), image: nil) } - func getSnapshot( - in context: Context, - completion: @escaping @Sendable (ImageEntry) -> Void - ) { + func snapshot( + for configuration: MemoryConfigurationAppIntent, + in context: Context + ) async -> ImageEntry { let cacheKey = "memory_\(context.family.rawValue)" - Task { - guard let api = try? await ImmichAPI() else { - completion( - ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first! - ) - return - } - - guard let memories = try? await api.fetchMemory(for: Date.now) - else { - completion(ImageEntry.handleError(for: cacheKey).entries.first!) - return - } - - for memory in memories { - if let asset = memory.assets.first(where: { $0.type == .image }), - let entry = try? await ImageEntry.build( - api: api, - asset: asset, - dateOffset: 0, - subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year) - ) - { - completion(entry) - return - } - } - - // fallback to random image - guard - let randomImage = try? await api.fetchSearchResults().first, - let imageEntry = try? await ImageEntry.build( - api: api, - asset: randomImage, - dateOffset: 0 - ) - else { - completion(ImageEntry.handleError(for: cacheKey).entries.first!) - return - } - - completion(imageEntry) + guard let api = try? await ImmichAPI() else { + return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries + .first! } + + guard let memories = try? await api.fetchMemory(for: Date.now) else { + return ImageEntry.handleError(for: cacheKey).entries.first! + } + + for memory in memories { + if let asset = memory.assets.first(where: { $0.type == .image }), + let entry = try? await ImageEntry.build( + api: api, + asset: asset, + dateOffset: 0, + subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year), + forceFullColor: configuration.forceFullColor + ) + { + return entry + } + } + + // fallback to random image + guard + let randomImage = try? await api.fetchSearchResults().first, + let imageEntry = try? await ImageEntry.build( + api: api, + asset: randomImage, + dateOffset: 0, + forceFullColor: configuration.forceFullColor + ) + else { + return ImageEntry.handleError(for: cacheKey).entries.first! + } + + return imageEntry } - func getTimeline( - in context: Context, - completion: @escaping @Sendable (Timeline) -> Void - ) { - Task { - var entries: [ImageEntry] = [] - let now = Date() + func timeline( + for configuration: MemoryConfigurationAppIntent, + in context: Context + ) async + -> Timeline + { + var entries: [ImageEntry] = [] + let now = Date() - let cacheKey = "memory_\(context.family.rawValue)" + let cacheKey = "memory_\(context.family.rawValue)" - guard let api = try? await ImmichAPI() else { - completion( - ImageEntry.handleError(for: cacheKey, error: .noLogin) - ) - return - } - - let memories: [MemoryResult] - do { - memories = try await api.fetchMemory(for: Date.now) - - await withTaskGroup(of: ImageEntry?.self) { group in - var totalAssets = 0 - - for memory in memories { - for asset in memory.assets { - if asset.type == .image && totalAssets < 12 { - group.addTask { - try? await ImageEntry.build( - api: api, - asset: asset, - dateOffset: totalAssets, - subtitle: getYearDifferenceSubtitle( - assetYear: memory.data.year - ) - ) - } - - totalAssets += 1 - } - } - } - - for await result in group { - if let entry = result { - entries.append(entry) - } - } - } - - } catch { - completion(ImageEntry.handleError(for: cacheKey)) - return - } - - // If we didn't add any memory images (some failure occurred or no images in memory), - // default to 12 hours of random photos - if entries.count == 0 { - // this must be a do/catch since we need to - // distinguish between a network fail and an empty search - do { - let search = try await generateRandomEntries( - api: api, - now: now, - count: 12 - ) - - // Load or save a cached asset for when network conditions are bad - if search.count == 0 { - completion( - ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable) - ) - return - } - - entries.append(contentsOf: search) - } catch { - completion(ImageEntry.handleError(for: cacheKey)) - return - } - } - - // cache the last image - try? entries.last!.cache(for: cacheKey) - - completion(Timeline(entries: entries, policy: .atEnd)) + guard let api = try? await ImmichAPI() else { + return ImageEntry.handleError(for: cacheKey, error: .noLogin) } + + let memories: [MemoryResult] + do { + memories = try await api.fetchMemory(for: Date.now) + + await withTaskGroup(of: ImageEntry?.self) { group in + var totalAssets = 0 + + for memory in memories { + for asset in memory.assets { + if asset.type == .image && totalAssets < 12 { + group.addTask { + try? await ImageEntry.build( + api: api, + asset: asset, + dateOffset: totalAssets, + subtitle: getYearDifferenceSubtitle( + assetYear: memory.data.year + ), + forceFullColor: configuration.forceFullColor + ) + } + + totalAssets += 1 + } + } + } + + for await result in group { + if let entry = result { + entries.append(entry) + } + } + } + + } catch { + return ImageEntry.handleError(for: cacheKey) + } + + // If we didn't add any memory images (some failure occurred or no images in memory), + // default to 12 hours of random photos + if entries.count == 0 { + // this must be a do/catch since we need to + // distinguish between a network fail and an empty search + do { + let search = try await generateRandomEntries( + api: api, + now: now, + count: 12, + forceFullColor: configuration.forceFullColor + ) + + // Load or save a cached asset for when network conditions are bad + if search.count == 0 { + return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable) + } + + entries.append(contentsOf: search) + } catch { + return ImageEntry.handleError(for: cacheKey) + } + } + + // cache the last image + try? entries.last!.cache(for: cacheKey) + + return Timeline(entries: entries, policy: .atEnd) } } @@ -160,8 +162,9 @@ struct ImmichMemoryWidget: Widget { let kind: String = "com.immich.widget.memory" var body: some WidgetConfiguration { - StaticConfiguration( + AppIntentConfiguration( kind: kind, + intent: MemoryConfigurationAppIntent.self, provider: ImmichMemoryProvider() ) { entry in ImmichWidgetView(entry: entry) diff --git a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift index 37f3c5e596..d987464152 100644 --- a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift +++ b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift @@ -47,6 +47,9 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent { @Parameter(title: "Show Album Name", default: false) var showAlbumName: Bool + + @Parameter(title: "Always Display in Full Color", default: false) + var forceFullColor: Bool } // MARK: Provider @@ -76,7 +79,8 @@ struct ImmichRandomProvider: AppIntentTimelineProvider { let entry = try? await ImageEntry.build( api: api, asset: randomImage, - dateOffset: 0 + dateOffset: 0, + forceFullColor: configuration.forceFullColor ) else { return ImageEntry.handleError(for: cacheKey).entries.first! @@ -114,7 +118,8 @@ struct ImmichRandomProvider: AppIntentTimelineProvider { now: now, count: 12, filter: album.filter, - subtitle: configuration.showAlbumName ? albumName : nil + subtitle: configuration.showAlbumName ? albumName : nil, + forceFullColor: configuration.forceFullColor ) // Load or save a cached asset for when network conditions are bad diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart index 7a1f46d198..9ad7b8c739 100644 --- a/mobile/lib/constants/aspect_ratios.dart +++ b/mobile/lib/constants/aspect_ratios.dart @@ -42,8 +42,8 @@ class CropAspectRatio { } } -const aspectRatioFree = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free); -const aspectRatioOriginal = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original); +const aspectRatioFree = CropAspectRatio.free; +const aspectRatioOriginal = CropAspectRatio.original; final aspectRatioPresets = [ CropAspectRatio.free, diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index d689e6cc1e..429a3718da 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -7,7 +7,7 @@ const Map locales = { 'Arabic (ar)': Locale('ar'), 'Basque (eu)': Locale('eu'), 'Belarusian (be)': Locale('be'), - 'Bosnian (bl)': Locale('bn'), + 'Bengali (bn)': Locale('bn'), 'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'), 'Bulgarian (bg)': Locale('bg'), 'Catalan (ca)': Locale('ca'), diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart index 63f4ed6be3..e63a3774e1 100644 --- a/mobile/lib/domain/models/album/album.model.dart +++ b/mobile/lib/domain/models/album/album.model.dart @@ -1,3 +1,8 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'album.model.freezed.dart'; + enum AlbumAssetOrder { // do not change this order! asc, @@ -12,118 +17,20 @@ enum AlbumUserRole { } // Model for an album stored in the server -class RemoteAlbum { - final String id; - final String name; - final String ownerId; - final String description; - final DateTime createdAt; - final DateTime updatedAt; - final String? thumbnailAssetId; - final bool isActivityEnabled; - final AlbumAssetOrder order; - final int assetCount; - final String ownerName; - final bool isShared; - - const RemoteAlbum({ - required this.id, - required this.name, - required this.ownerId, - required this.description, - required this.createdAt, - required this.updatedAt, - this.thumbnailAssetId, - required this.isActivityEnabled, - required this.order, - required this.assetCount, - required this.ownerName, - required this.isShared, - }); - - @override - String toString() { - return '''Album { - id: $id, - name: $name, - ownerId: $ownerId, - description: $description, - createdAt: $createdAt, - updatedAt: $updatedAt, - isActivityEnabled: $isActivityEnabled, - order: $order, - thumbnailAssetId: ${thumbnailAssetId ?? ""} - assetCount: $assetCount - ownerName: $ownerName - isShared: $isShared - }'''; - } - - @override - bool operator ==(Object other) { - if (other is! RemoteAlbum) { - return false; - } - if (identical(this, other)) { - return true; - } - return id == other.id && - name == other.name && - ownerId == other.ownerId && - description == other.description && - createdAt == other.createdAt && - updatedAt == other.updatedAt && - thumbnailAssetId == other.thumbnailAssetId && - isActivityEnabled == other.isActivityEnabled && - order == other.order && - assetCount == other.assetCount && - ownerName == other.ownerName && - isShared == other.isShared; - } - - @override - int get hashCode { - return id.hashCode ^ - name.hashCode ^ - ownerId.hashCode ^ - description.hashCode ^ - createdAt.hashCode ^ - updatedAt.hashCode ^ - thumbnailAssetId.hashCode ^ - isActivityEnabled.hashCode ^ - order.hashCode ^ - assetCount.hashCode ^ - ownerName.hashCode ^ - isShared.hashCode; - } - - RemoteAlbum copyWith({ - String? id, - String? name, - String? ownerId, - String? description, - DateTime? createdAt, - DateTime? updatedAt, +@freezed +abstract class RemoteAlbum with _$RemoteAlbum { + const factory RemoteAlbum({ + required String id, + required String name, + required String ownerId, + required String description, + required DateTime createdAt, + required DateTime updatedAt, String? thumbnailAssetId, - bool? isActivityEnabled, - AlbumAssetOrder? order, - int? assetCount, - String? ownerName, - bool? isShared, - }) { - return RemoteAlbum( - id: id ?? this.id, - name: name ?? this.name, - ownerId: ownerId ?? this.ownerId, - description: description ?? this.description, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, - isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, - order: order ?? this.order, - assetCount: assetCount ?? this.assetCount, - ownerName: ownerName ?? this.ownerName, - isShared: isShared ?? this.isShared, - ); - } + required bool isActivityEnabled, + required AlbumAssetOrder order, + required int assetCount, + required String ownerName, + required bool isShared, + }) = _RemoteAlbum; } diff --git a/mobile/lib/domain/models/album/local_album.model.dart b/mobile/lib/domain/models/album/local_album.model.dart index 9e8521fa02..cddc1d1953 100644 --- a/mobile/lib/domain/models/album/local_album.model.dart +++ b/mobile/lib/domain/models/album/local_album.model.dart @@ -1,3 +1,8 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'local_album.model.freezed.dart'; + enum BackupSelection { // Used to sort albums based on the backupSelection // selected -> none -> excluded @@ -7,85 +12,15 @@ enum BackupSelection { excluded, } -class LocalAlbum { - final String id; - final String name; - final DateTime updatedAt; - final bool isIosSharedAlbum; - - final int assetCount; - final BackupSelection backupSelection; - final String? linkedRemoteAlbumId; - - const LocalAlbum({ - required this.id, - required this.name, - required this.updatedAt, - this.assetCount = 0, - this.backupSelection = BackupSelection.none, - this.isIosSharedAlbum = false, - this.linkedRemoteAlbumId, - }); - - LocalAlbum copyWith({ - String? id, - String? name, - DateTime? updatedAt, - int? assetCount, - BackupSelection? backupSelection, - bool? isIosSharedAlbum, +@freezed +abstract class LocalAlbum with _$LocalAlbum { + const factory LocalAlbum({ + required String id, + required String name, + required DateTime updatedAt, + @Default(0) int assetCount, + @Default(BackupSelection.none) BackupSelection backupSelection, + @Default(false) bool isIosSharedAlbum, String? linkedRemoteAlbumId, - }) { - return LocalAlbum( - id: id ?? this.id, - name: name ?? this.name, - updatedAt: updatedAt ?? this.updatedAt, - assetCount: assetCount ?? this.assetCount, - backupSelection: backupSelection ?? this.backupSelection, - isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, - linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, - ); - } - - @override - bool operator ==(Object other) { - if (other is! LocalAlbum) { - return false; - } - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.name == name && - other.updatedAt == updatedAt && - other.assetCount == assetCount && - other.backupSelection == backupSelection && - other.isIosSharedAlbum == isIosSharedAlbum && - other.linkedRemoteAlbumId == linkedRemoteAlbumId; - } - - @override - int get hashCode { - return id.hashCode ^ - name.hashCode ^ - updatedAt.hashCode ^ - assetCount.hashCode ^ - backupSelection.hashCode ^ - isIosSharedAlbum.hashCode ^ - linkedRemoteAlbumId.hashCode; - } - - @override - String toString() { - return '''LocalAlbum: { -id: $id, -name: $name, -updatedAt: $updatedAt, -assetCount: $assetCount, -backupSelection: $backupSelection, -isIosSharedAlbum: $isIosSharedAlbum -linkedRemoteAlbumId: $linkedRemoteAlbumId, -}'''; - } + }) = _LocalAlbum; } diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index ea6f0ab287..d7d74daa25 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -53,7 +53,7 @@ sealed class BaseAsset { if (durationMs != null) { return Duration(milliseconds: durationMs); } - return const Duration(); + return Duration.zero; } bool get hasRemote => storage == AssetState.remote || storage == AssetState.merged; diff --git a/mobile/lib/domain/models/asset_edit.model.dart b/mobile/lib/domain/models/asset_edit.model.dart index 9809b9c606..c7a09dbae3 100644 --- a/mobile/lib/domain/models/asset_edit.model.dart +++ b/mobile/lib/domain/models/asset_edit.model.dart @@ -1,4 +1,4 @@ -import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters; +import "package:openapi/api.dart" show CropParameters, MirrorParameters, RotateParameters; enum AssetEditAction { rotate, crop, mirror, other } diff --git a/mobile/lib/domain/models/asset_face.model.dart b/mobile/lib/domain/models/asset_face.model.dart index 1388836946..c65f159fe7 100644 --- a/mobile/lib/domain/models/asset_face.model.dart +++ b/mobile/lib/domain/models/asset_face.model.dart @@ -1,100 +1,21 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'asset_face.model.freezed.dart'; + // Model for an asset face stored in the server -class AssetFace { - final String id; - final String assetId; - final String? personId; - final int imageWidth; - final int imageHeight; - final int boundingBoxX1; - final int boundingBoxY1; - final int boundingBoxX2; - final int boundingBoxY2; - final String sourceType; - - const AssetFace({ - required this.id, - required this.assetId, - this.personId, - required this.imageWidth, - required this.imageHeight, - required this.boundingBoxX1, - required this.boundingBoxY1, - required this.boundingBoxX2, - required this.boundingBoxY2, - required this.sourceType, - }); - - AssetFace copyWith({ - String? id, - String? assetId, +@freezed +abstract class AssetFace with _$AssetFace { + const factory AssetFace({ + required String id, + required String assetId, String? personId, - int? imageWidth, - int? imageHeight, - int? boundingBoxX1, - int? boundingBoxY1, - int? boundingBoxX2, - int? boundingBoxY2, - String? sourceType, - }) { - return AssetFace( - id: id ?? this.id, - assetId: assetId ?? this.assetId, - personId: personId ?? this.personId, - imageWidth: imageWidth ?? this.imageWidth, - imageHeight: imageHeight ?? this.imageHeight, - boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, - boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, - boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, - boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, - sourceType: sourceType ?? this.sourceType, - ); - } - - @override - String toString() { - return '''AssetFace { - id: $id, - assetId: $assetId, - personId: ${personId ?? ""}, - imageWidth: $imageWidth, - imageHeight: $imageHeight, - boundingBoxX1: $boundingBoxX1, - boundingBoxY1: $boundingBoxY1, - boundingBoxX2: $boundingBoxX2, - boundingBoxY2: $boundingBoxY2, - sourceType: $sourceType, -}'''; - } - - @override - bool operator ==(covariant AssetFace other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.assetId == assetId && - other.personId == personId && - other.imageWidth == imageWidth && - other.imageHeight == imageHeight && - other.boundingBoxX1 == boundingBoxX1 && - other.boundingBoxY1 == boundingBoxY1 && - other.boundingBoxX2 == boundingBoxX2 && - other.boundingBoxY2 == boundingBoxY2 && - other.sourceType == sourceType; - } - - @override - int get hashCode { - return id.hashCode ^ - assetId.hashCode ^ - personId.hashCode ^ - imageWidth.hashCode ^ - imageHeight.hashCode ^ - boundingBoxX1.hashCode ^ - boundingBoxY1.hashCode ^ - boundingBoxX2.hashCode ^ - boundingBoxY2.hashCode ^ - sourceType.hashCode; - } + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) = _AssetFace; } diff --git a/mobile/lib/domain/models/config/album_config.dart b/mobile/lib/domain/models/config/album_config.dart index a83fc32fbf..16deca1b8f 100644 --- a/mobile/lib/domain/models/config/album_config.dart +++ b/mobile/lib/domain/models/config/album_config.dart @@ -1,26 +1,14 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -class AlbumConfig { - final AlbumSortMode sortMode; - final bool isReverse; - final bool isGrid; +part 'album_config.freezed.dart'; - const AlbumConfig({this.sortMode = AlbumSortMode.mostRecent, this.isReverse = true, this.isGrid = false}); - - AlbumConfig copyWith({AlbumSortMode? sortMode, bool? isReverse, bool? isGrid}) => AlbumConfig( - sortMode: sortMode ?? this.sortMode, - isReverse: isReverse ?? this.isReverse, - isGrid: isGrid ?? this.isGrid, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AlbumConfig && other.sortMode == sortMode && other.isReverse == isReverse && other.isGrid == isGrid); - - @override - int get hashCode => Object.hash(sortMode, isReverse, isGrid); - - @override - String toString() => 'AlbumConfig(sortMode: $sortMode, isReverse: $isReverse, isGrid: $isGrid)'; +@freezed +abstract class AlbumConfig with _$AlbumConfig { + const factory AlbumConfig({ + @Default(AlbumSortMode.mostRecent) AlbumSortMode sortMode, + @Default(true) bool isReverse, + @Default(false) bool isGrid, + }) = _AlbumConfig; } diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index e4e11baf9d..bc089e621e 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -1,4 +1,6 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/config/album_config.dart'; @@ -19,107 +21,29 @@ import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/utils/semver.dart'; +part 'app_config.freezed.dart'; + const defaultConfig = AppConfig(); -class AppConfig { - final LogLevel logLevel; - final ThemeConfig theme; - final CleanupConfig cleanup; - final MapConfig map; - final TimelineConfig timeline; - final ImageConfig image; - final ViewerConfig viewer; - final SlideshowConfig slideshow; - final AlbumConfig album; - final BackupConfig backup; - final NetworkConfig network; - final ShareConfig share; - final FeatureMessageConfig featureMessage; +@freezed +abstract class AppConfig with _$AppConfig { + const AppConfig._(); - const AppConfig({ - this.logLevel = .info, - this.theme = const .new(), - this.cleanup = const .new(), - this.map = const .new(), - this.timeline = const .new(), - this.image = const .new(), - this.viewer = const .new(), - this.slideshow = const .new(), - this.album = const .new(), - this.backup = const .new(), - this.network = const .new(), - this.share = const .new(), - this.featureMessage = const .new(), - }); - - AppConfig copyWith({ - LogLevel? logLevel, - ThemeConfig? theme, - CleanupConfig? cleanup, - MapConfig? map, - TimelineConfig? timeline, - ImageConfig? image, - ViewerConfig? viewer, - SlideshowConfig? slideshow, - AlbumConfig? album, - BackupConfig? backup, - NetworkConfig? network, - ShareConfig? share, - FeatureMessageConfig? featureMessage, - }) => .new( - logLevel: logLevel ?? this.logLevel, - theme: theme ?? this.theme, - cleanup: cleanup ?? this.cleanup, - map: map ?? this.map, - timeline: timeline ?? this.timeline, - image: image ?? this.image, - viewer: viewer ?? this.viewer, - slideshow: slideshow ?? this.slideshow, - album: album ?? this.album, - backup: backup ?? this.backup, - network: network ?? this.network, - share: share ?? this.share, - featureMessage: featureMessage ?? this.featureMessage, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AppConfig && - other.logLevel == logLevel && - other.theme == theme && - other.cleanup == cleanup && - other.map == map && - other.timeline == timeline && - other.image == image && - other.viewer == viewer && - other.slideshow == slideshow && - other.album == album && - other.backup == backup && - other.network == network && - other.share == share && - other.featureMessage == featureMessage); - - @override - int get hashCode => Object.hash( - logLevel, - theme, - cleanup, - map, - timeline, - image, - viewer, - slideshow, - album, - backup, - network, - share, - featureMessage, - ); - - @override - String toString() => - 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share, featureMessage: $featureMessage)'; + const factory AppConfig({ + @Default(LogLevel.info) LogLevel logLevel, + @Default(ThemeConfig()) ThemeConfig theme, + @Default(CleanupConfig()) CleanupConfig cleanup, + @Default(MapConfig()) MapConfig map, + @Default(TimelineConfig()) TimelineConfig timeline, + @Default(ImageConfig()) ImageConfig image, + @Default(ViewerConfig()) ViewerConfig viewer, + @Default(SlideshowConfig()) SlideshowConfig slideshow, + @Default(AlbumConfig()) AlbumConfig album, + @Default(BackupConfig()) BackupConfig backup, + @Default(NetworkConfig()) NetworkConfig network, + @Default(ShareConfig()) ShareConfig share, + @Default(FeatureMessageConfig()) FeatureMessageConfig featureMessage, + }) = _AppConfig; T read(SettingsKey key) => (switch (key) { @@ -190,9 +114,9 @@ class AppConfig { .viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)), .networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)), .networkPreferredWifiName => copyWith( - network: network.copyWith(preferredWifiName: .fromNullable((value as String?))), + network: network.copyWith(preferredWifiName: .fromNullable(value as String?)), ), - .networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: .fromNullable((value as String?)))), + .networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: .fromNullable(value as String?))), .networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List)), .networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map)), .albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)), diff --git a/mobile/lib/domain/models/config/backup_config.dart b/mobile/lib/domain/models/config/backup_config.dart index 19f91a4ed7..4177408fcc 100644 --- a/mobile/lib/domain/models/config/backup_config.dart +++ b/mobile/lib/domain/models/config/backup_config.dart @@ -1,52 +1,16 @@ -class BackupConfig { - final bool enabled; - final bool useCellularForVideos; - final bool useCellularForPhotos; - final bool requireCharging; - final int triggerDelay; - final bool syncAlbums; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; - const BackupConfig({ - this.enabled = false, - this.useCellularForVideos = false, - this.useCellularForPhotos = false, - this.requireCharging = false, - this.triggerDelay = 30, - this.syncAlbums = false, - }); +part 'backup_config.freezed.dart'; - BackupConfig copyWith({ - bool? enabled, - bool? useCellularForVideos, - bool? useCellularForPhotos, - bool? requireCharging, - int? triggerDelay, - bool? syncAlbums, - }) => BackupConfig( - enabled: enabled ?? this.enabled, - useCellularForVideos: useCellularForVideos ?? this.useCellularForVideos, - useCellularForPhotos: useCellularForPhotos ?? this.useCellularForPhotos, - requireCharging: requireCharging ?? this.requireCharging, - triggerDelay: triggerDelay ?? this.triggerDelay, - syncAlbums: syncAlbums ?? this.syncAlbums, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is BackupConfig && - other.enabled == enabled && - other.useCellularForVideos == useCellularForVideos && - other.useCellularForPhotos == useCellularForPhotos && - other.requireCharging == requireCharging && - other.triggerDelay == triggerDelay && - other.syncAlbums == syncAlbums); - - @override - int get hashCode => - Object.hash(enabled, useCellularForVideos, useCellularForPhotos, requireCharging, triggerDelay, syncAlbums); - - @override - String toString() => - 'BackupConfig(enabled: $enabled, useCellularForVideos: $useCellularForVideos, useCellularForPhotos: $useCellularForPhotos, requireCharging: $requireCharging, triggerDelay: $triggerDelay, syncAlbums: $syncAlbums)'; +@freezed +abstract class BackupConfig with _$BackupConfig { + const factory BackupConfig({ + @Default(false) bool enabled, + @Default(false) bool useCellularForVideos, + @Default(false) bool useCellularForPhotos, + @Default(false) bool requireCharging, + @Default(30) int triggerDelay, + @Default(false) bool syncAlbums, + }) = _BackupConfig; } diff --git a/mobile/lib/domain/models/config/feature_message_config.dart b/mobile/lib/domain/models/config/feature_message_config.dart index d5c7c69550..0fc13fd51f 100644 --- a/mobile/lib/domain/models/config/feature_message_config.dart +++ b/mobile/lib/domain/models/config/feature_message_config.dart @@ -1,20 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/utils/semver.dart'; -class FeatureMessageConfig { - final SemVer seenRelease; +part 'feature_message_config.freezed.dart'; - const FeatureMessageConfig({this.seenRelease = const SemVer(major: 0, minor: 0, patch: 0)}); - - FeatureMessageConfig copyWith({SemVer? seenRelease}) => - FeatureMessageConfig(seenRelease: seenRelease ?? this.seenRelease); - - @override - bool operator ==(Object other) => - identical(this, other) || (other is FeatureMessageConfig && other.seenRelease == seenRelease); - - @override - int get hashCode => seenRelease.hashCode; - - @override - String toString() => 'FeatureMessageConfig(seenRelease: $seenRelease)'; +@freezed +abstract class FeatureMessageConfig with _$FeatureMessageConfig { + const factory FeatureMessageConfig({@Default(SemVer(major: 0, minor: 0, patch: 0)) SemVer seenRelease}) = + _FeatureMessageConfig; } diff --git a/mobile/lib/domain/models/config/image_config.dart b/mobile/lib/domain/models/config/image_config.dart index 8410a9010b..a51193d1d3 100644 --- a/mobile/lib/domain/models/config/image_config.dart +++ b/mobile/lib/domain/models/config/image_config.dart @@ -1,20 +1,9 @@ -class ImageConfig { - final bool preferRemote; - final bool loadOriginal; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; - const ImageConfig({this.preferRemote = false, this.loadOriginal = false}); +part 'image_config.freezed.dart'; - ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) => - ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal); - - @override - int get hashCode => Object.hash(preferRemote, loadOriginal); - - @override - String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)'; +@freezed +abstract class ImageConfig with _$ImageConfig { + const factory ImageConfig({@Default(false) bool preferRemote, @Default(false) bool loadOriginal}) = _ImageConfig; } diff --git a/mobile/lib/domain/models/config/share_config.dart b/mobile/lib/domain/models/config/share_config.dart index 898867ff78..49530aca30 100644 --- a/mobile/lib/domain/models/config/share_config.dart +++ b/mobile/lib/domain/models/config/share_config.dart @@ -1,18 +1,10 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/constants/enums.dart'; -class ShareConfig { - final ShareAssetType fileType; +part 'share_config.freezed.dart'; - const ShareConfig({this.fileType = ShareAssetType.original}); - - ShareConfig copyWith({ShareAssetType? fileType}) => ShareConfig(fileType: fileType ?? this.fileType); - - @override - bool operator ==(Object other) => identical(this, other) || (other is ShareConfig && other.fileType == fileType); - - @override - int get hashCode => fileType.hashCode; - - @override - String toString() => 'ShareConfig(fileType: $fileType)'; +@freezed +abstract class ShareConfig with _$ShareConfig { + const factory ShareConfig({@Default(ShareAssetType.original) ShareAssetType fileType}) = _ShareConfig; } diff --git a/mobile/lib/domain/models/config/slideshow_config.dart b/mobile/lib/domain/models/config/slideshow_config.dart index 6bcdaadc77..cacba1a89e 100644 --- a/mobile/lib/domain/models/config/slideshow_config.dart +++ b/mobile/lib/domain/models/config/slideshow_config.dart @@ -1,38 +1,15 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/constants/enums.dart'; -class SlideshowConfig { - final bool repeat; - final int duration; - final SlideshowLook look; - final SlideshowDirection direction; +part 'slideshow_config.freezed.dart'; - const SlideshowConfig({ - this.repeat = true, - this.duration = 5, - this.look = SlideshowLook.blurredBackground, - this.direction = SlideshowDirection.forward, - }); - - SlideshowConfig copyWith({bool? repeat, int? duration, SlideshowLook? look, SlideshowDirection? direction}) => - SlideshowConfig( - repeat: repeat ?? this.repeat, - duration: duration ?? this.duration, - look: look ?? this.look, - direction: direction ?? this.direction, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is SlideshowConfig && - other.repeat == repeat && - other.duration == duration && - other.look == look && - other.direction == direction); - - @override - int get hashCode => Object.hash(repeat, duration, look, direction); - - @override - String toString() => 'SlideshowConfig(repeat: $repeat, duration: $duration, look: $look, direction: $direction)'; +@freezed +abstract class SlideshowConfig with _$SlideshowConfig { + const factory SlideshowConfig({ + @Default(true) bool repeat, + @Default(5) int duration, + @Default(SlideshowLook.blurredBackground) SlideshowLook look, + @Default(SlideshowDirection.forward) SlideshowDirection direction, + }) = _SlideshowConfig; } diff --git a/mobile/lib/domain/models/config/theme_config.dart b/mobile/lib/domain/models/config/theme_config.dart index fa955c5d46..ac651a8062 100644 --- a/mobile/lib/domain/models/config/theme_config.dart +++ b/mobile/lib/domain/models/config/theme_config.dart @@ -1,44 +1,16 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/constants/colors.dart'; -class ThemeConfig { - final ThemeMode mode; - final ImmichColorPreset primaryColor; - final bool dynamicTheme; - final bool colorfulInterface; +part 'theme_config.freezed.dart'; - const ThemeConfig({ - this.mode = .system, - this.primaryColor = .indigo, - this.dynamicTheme = false, - this.colorfulInterface = true, - }); - - ThemeConfig copyWith({ - ThemeMode? mode, - ImmichColorPreset? primaryColor, - bool? dynamicTheme, - bool? colorfulInterface, - }) => .new( - mode: mode ?? this.mode, - primaryColor: primaryColor ?? this.primaryColor, - dynamicTheme: dynamicTheme ?? this.dynamicTheme, - colorfulInterface: colorfulInterface ?? this.colorfulInterface, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is ThemeConfig && - other.mode == mode && - other.primaryColor == primaryColor && - other.dynamicTheme == dynamicTheme && - other.colorfulInterface == colorfulInterface); - - @override - int get hashCode => Object.hash(mode, primaryColor, dynamicTheme, colorfulInterface); - - @override - String toString() => - 'ThemeConfig(mode: $mode, primaryColor: $primaryColor, dynamicTheme: $dynamicTheme, colorfulInterface: $colorfulInterface)'; +@freezed +abstract class ThemeConfig with _$ThemeConfig { + const factory ThemeConfig({ + @Default(ThemeMode.system) ThemeMode mode, + @Default(ImmichColorPreset.indigo) ImmichColorPreset primaryColor, + @Default(false) bool dynamicTheme, + @Default(true) bool colorfulInterface, + }) = _ThemeConfig; } diff --git a/mobile/lib/domain/models/config/timeline_config.dart b/mobile/lib/domain/models/config/timeline_config.dart index 4b6b9d5625..c4681b0a62 100644 --- a/mobile/lib/domain/models/config/timeline_config.dart +++ b/mobile/lib/domain/models/config/timeline_config.dart @@ -1,30 +1,14 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; -class TimelineConfig { - final int tilesPerRow; - final GroupAssetsBy groupAssetsBy; - final bool storageIndicator; +part 'timeline_config.freezed.dart'; - const TimelineConfig({this.tilesPerRow = 4, this.groupAssetsBy = GroupAssetsBy.day, this.storageIndicator = true}); - - TimelineConfig copyWith({int? tilesPerRow, GroupAssetsBy? groupAssetsBy, bool? storageIndicator}) => TimelineConfig( - tilesPerRow: tilesPerRow ?? this.tilesPerRow, - groupAssetsBy: groupAssetsBy ?? this.groupAssetsBy, - storageIndicator: storageIndicator ?? this.storageIndicator, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is TimelineConfig && - other.tilesPerRow == tilesPerRow && - other.groupAssetsBy == groupAssetsBy && - other.storageIndicator == storageIndicator); - - @override - int get hashCode => Object.hash(tilesPerRow, groupAssetsBy, storageIndicator); - - @override - String toString() => - 'TimelineConfig(tilesPerRow: $tilesPerRow, groupAssetsBy: $groupAssetsBy, storageIndicator: $storageIndicator)'; +@freezed +abstract class TimelineConfig with _$TimelineConfig { + const factory TimelineConfig({ + @Default(4) int tilesPerRow, + @Default(GroupAssetsBy.day) GroupAssetsBy groupAssetsBy, + @Default(true) bool storageIndicator, + }) = _TimelineConfig; } diff --git a/mobile/lib/domain/models/config/viewer_config.dart b/mobile/lib/domain/models/config/viewer_config.dart index 595f2bee5d..e799843034 100644 --- a/mobile/lib/domain/models/config/viewer_config.dart +++ b/mobile/lib/domain/models/config/viewer_config.dart @@ -1,37 +1,14 @@ -class ViewerConfig { - final bool loopVideo; - final bool loadOriginalVideo; - final bool autoPlayVideo; - final bool tapToNavigate; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; - const ViewerConfig({ - this.loopVideo = true, - this.loadOriginalVideo = false, - this.autoPlayVideo = true, - this.tapToNavigate = false, - }); +part 'viewer_config.freezed.dart'; - ViewerConfig copyWith({bool? loopVideo, bool? loadOriginalVideo, bool? autoPlayVideo, bool? tapToNavigate}) => - ViewerConfig( - loopVideo: loopVideo ?? this.loopVideo, - loadOriginalVideo: loadOriginalVideo ?? this.loadOriginalVideo, - autoPlayVideo: autoPlayVideo ?? this.autoPlayVideo, - tapToNavigate: tapToNavigate ?? this.tapToNavigate, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is ViewerConfig && - other.loopVideo == loopVideo && - other.loadOriginalVideo == loadOriginalVideo && - other.autoPlayVideo == autoPlayVideo && - other.tapToNavigate == tapToNavigate); - - @override - int get hashCode => Object.hash(loopVideo, loadOriginalVideo, autoPlayVideo, tapToNavigate); - - @override - String toString() => - 'ViewerConfig(loopVideo: $loopVideo, loadOriginalVideo: $loadOriginalVideo, autoPlayVideo: $autoPlayVideo, tapToNavigate: $tapToNavigate)'; +@freezed +abstract class ViewerConfig with _$ViewerConfig { + const factory ViewerConfig({ + @Default(true) bool loopVideo, + @Default(false) bool loadOriginalVideo, + @Default(true) bool autoPlayVideo, + @Default(false) bool tapToNavigate, + }) = _ViewerConfig; } diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart index 4284aef2ab..3536048ecc 100644 --- a/mobile/lib/domain/models/exif.model.dart +++ b/mobile/lib/domain/models/exif.model.dart @@ -1,30 +1,40 @@ -class ExifInfo { - final int? assetId; - final int? fileSize; - final String? description; - final bool isFlipped; - final String? orientation; - final String? timeZone; - final DateTime? dateTimeOriginal; - final int? rating; - final int? width; - final int? height; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; - // GPS - final double? latitude; - final double? longitude; - final String? city; - final String? state; - final String? country; +part 'exif.model.freezed.dart'; - // Camera related - final String? make; - final String? model; - final String? lens; - final double? f; - final double? mm; - final int? iso; - final double? exposureSeconds; +@freezed +abstract class ExifInfo with _$ExifInfo { + const ExifInfo._(); + + const factory ExifInfo({ + int? assetId, + int? fileSize, + String? description, + @Default(false) bool isFlipped, + String? orientation, + String? timeZone, + DateTime? dateTimeOriginal, + int? rating, + int? width, + int? height, + + // GPS + double? latitude, + double? longitude, + String? city, + String? state, + String? country, + + // Camera related + String? make, + String? model, + String? lens, + double? f, + double? mm, + int? iso, + double? exposureSeconds, + }) = _ExifInfo; bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0; @@ -41,162 +51,4 @@ class ExifInfo { String get fNumber => f == null ? "" : f!.toStringAsFixed(1); String get focalLength => mm == null ? "" : mm!.toStringAsFixed(3); - - const ExifInfo({ - this.assetId, - this.fileSize, - this.description, - this.orientation, - this.timeZone, - this.dateTimeOriginal, - this.rating, - this.width, - this.height, - this.isFlipped = false, - this.latitude, - this.longitude, - this.city, - this.state, - this.country, - this.make, - this.model, - this.lens, - this.f, - this.mm, - this.iso, - this.exposureSeconds, - }); - - @override - bool operator ==(covariant ExifInfo other) { - if (identical(this, other)) { - return true; - } - - return other.fileSize == fileSize && - other.description == description && - other.isFlipped == isFlipped && - other.orientation == orientation && - other.timeZone == timeZone && - other.dateTimeOriginal == dateTimeOriginal && - other.rating == rating && - other.width == width && - other.height == height && - other.latitude == latitude && - other.longitude == longitude && - other.city == city && - other.state == state && - other.country == country && - other.make == make && - other.model == model && - other.lens == lens && - other.f == f && - other.mm == mm && - other.iso == iso && - other.exposureSeconds == exposureSeconds && - other.assetId == assetId; - } - - @override - int get hashCode { - return fileSize.hashCode ^ - description.hashCode ^ - orientation.hashCode ^ - isFlipped.hashCode ^ - timeZone.hashCode ^ - dateTimeOriginal.hashCode ^ - rating.hashCode ^ - width.hashCode ^ - height.hashCode ^ - latitude.hashCode ^ - longitude.hashCode ^ - city.hashCode ^ - state.hashCode ^ - country.hashCode ^ - make.hashCode ^ - model.hashCode ^ - lens.hashCode ^ - f.hashCode ^ - mm.hashCode ^ - iso.hashCode ^ - exposureSeconds.hashCode ^ - assetId.hashCode; - } - - @override - String toString() { - return '''{ -fileSize: ${fileSize ?? 'NA'}, -description: ${description ?? 'NA'}, -orientation: ${orientation ?? 'NA'}, -isFlipped: $isFlipped, -timeZone: ${timeZone ?? 'NA'}, -dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, -rating: ${rating ?? 'NA'}, -width: ${width ?? 'NA'}, -height: ${height ?? 'NA'}, -latitude: ${latitude ?? 'NA'}, -longitude: ${longitude ?? 'NA'}, -city: ${city ?? 'NA'}, -state: ${state ?? 'NA'}, -country: ${country ?? ''}, -make: ${make ?? 'NA'}, -model: ${model ?? 'NA'}, -lens: ${lens ?? 'NA'}, -f: ${f ?? 'NA'}, -mm: ${mm ?? ''}, -iso: ${iso ?? 'NA'}, -exposureSeconds: ${exposureSeconds ?? 'NA'}, -}'''; - } - - ExifInfo copyWith({ - int? assetId, - int? fileSize, - String? description, - String? orientation, - String? timeZone, - DateTime? dateTimeOriginal, - int? rating, - int? width, - int? height, - double? latitude, - double? longitude, - String? city, - String? state, - String? country, - bool? isFlipped, - String? make, - String? model, - String? lens, - double? f, - double? mm, - int? iso, - double? exposureSeconds, - }) { - return ExifInfo( - assetId: assetId ?? this.assetId, - fileSize: fileSize ?? this.fileSize, - description: description ?? this.description, - orientation: orientation ?? this.orientation, - timeZone: timeZone ?? this.timeZone, - dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, - rating: rating ?? this.rating, - width: width ?? this.width, - height: height ?? this.height, - isFlipped: isFlipped ?? this.isFlipped, - latitude: latitude ?? this.latitude, - longitude: longitude ?? this.longitude, - city: city ?? this.city, - state: state ?? this.state, - country: country ?? this.country, - make: make ?? this.make, - model: model ?? this.model, - lens: lens ?? this.lens, - f: f ?? this.f, - mm: mm ?? this.mm, - iso: iso ?? this.iso, - exposureSeconds: exposureSeconds ?? this.exposureSeconds, - ); - } } diff --git a/mobile/lib/domain/models/ocr.model.dart b/mobile/lib/domain/models/ocr.model.dart index 403c06d44e..bfb398bc79 100644 --- a/mobile/lib/domain/models/ocr.model.dart +++ b/mobile/lib/domain/models/ocr.model.dart @@ -1,128 +1,24 @@ -class Ocr { - final String id; - final String assetId; - final double x1; - final double y1; - final double x2; - final double y2; - final double x3; - final double y3; - final double x4; - final double y4; - final double boxScore; - final double textScore; - final String text; - final bool isVisible; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; - const Ocr({ - required this.id, - required this.assetId, - required this.x1, - required this.y1, - required this.x2, - required this.y2, - required this.x3, - required this.y3, - required this.x4, - required this.y4, - required this.boxScore, - required this.textScore, - required this.text, - required this.isVisible, - }); +part 'ocr.model.freezed.dart'; - Ocr copyWith({ - String? id, - String? assetId, - double? x1, - double? y1, - double? x2, - double? y2, - double? x3, - double? y3, - double? x4, - double? y4, - double? boxScore, - double? textScore, - String? text, - bool? isVisible, - }) { - return Ocr( - id: id ?? this.id, - assetId: assetId ?? this.assetId, - x1: x1 ?? this.x1, - y1: y1 ?? this.y1, - x2: x2 ?? this.x2, - y2: y2 ?? this.y2, - x3: x3 ?? this.x3, - y3: y3 ?? this.y3, - x4: x4 ?? this.x4, - y4: y4 ?? this.y4, - boxScore: boxScore ?? this.boxScore, - textScore: textScore ?? this.textScore, - text: text ?? this.text, - isVisible: isVisible ?? this.isVisible, - ); - } - - @override - String toString() { - return '''Ocr { - id: $id, - assetId: $assetId, - x1: $x1, - y1: $y1, - x2: $x2, - y2: $y2, - x3: $x3, - y3: $y3, - x4: $x4, - y4: $y4, - boxScore: $boxScore, - textScore: $textScore, - text: $text, - isVisible: $isVisible - }'''; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is Ocr && - other.id == id && - other.assetId == assetId && - other.x1 == x1 && - other.y1 == y1 && - other.x2 == x2 && - other.y2 == y2 && - other.x3 == x3 && - other.y3 == y3 && - other.x4 == x4 && - other.y4 == y4 && - other.boxScore == boxScore && - other.textScore == textScore && - other.text == text && - other.isVisible == isVisible; - } - - @override - int get hashCode { - return id.hashCode ^ - assetId.hashCode ^ - x1.hashCode ^ - y1.hashCode ^ - x2.hashCode ^ - y2.hashCode ^ - x3.hashCode ^ - y3.hashCode ^ - x4.hashCode ^ - y4.hashCode ^ - boxScore.hashCode ^ - textScore.hashCode ^ - text.hashCode ^ - isVisible.hashCode; - } +@freezed +abstract class Ocr with _$Ocr { + const factory Ocr({ + required String id, + required String assetId, + required double x1, + required double y1, + required double x2, + required double y2, + required double x3, + required double y3, + required double x4, + required double y4, + required double boxScore, + required double textScore, + required String text, + required bool isVisible, + }) = _Ocr; } diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart index c7cdcff3af..38d740eeb4 100644 --- a/mobile/lib/domain/models/person.model.dart +++ b/mobile/lib/domain/models/person.model.dart @@ -1,5 +1,10 @@ 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({ @@ -93,102 +98,18 @@ class PersonDto { } // Model for a person stored in the server -class DriftPerson { - final String id; - final DateTime createdAt; - final DateTime updatedAt; - final String ownerId; - final String name; - final String? faceAssetId; - final bool isFavorite; - final bool isHidden; - final String? color; - final DateTime? birthDate; - - const DriftPerson({ - required this.id, - required this.createdAt, - required this.updatedAt, - required this.ownerId, - required this.name, - this.faceAssetId, - required this.isFavorite, - required this.isHidden, - required this.color, - this.birthDate, - }); - - DriftPerson copyWith({ - String? id, - DateTime? createdAt, - DateTime? updatedAt, - String? ownerId, - String? name, +@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, - bool? isFavorite, - bool? isHidden, - String? color, + required bool isFavorite, + required bool isHidden, + required String? color, DateTime? birthDate, - }) { - return DriftPerson( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ownerId: ownerId ?? this.ownerId, - name: name ?? this.name, - faceAssetId: faceAssetId ?? this.faceAssetId, - isFavorite: isFavorite ?? this.isFavorite, - isHidden: isHidden ?? this.isHidden, - color: color ?? this.color, - birthDate: birthDate ?? this.birthDate, - ); - } - - @override - String toString() { - return '''Person { - id: $id, - createdAt: $createdAt, - updatedAt: $updatedAt, - ownerId: $ownerId, - name: $name, - faceAssetId: ${faceAssetId ?? ""}, - isFavorite: $isFavorite, - isHidden: $isHidden, - color: ${color ?? ""}, - birthDate: ${birthDate ?? ""} -}'''; - } - - @override - bool operator ==(covariant DriftPerson other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.createdAt == createdAt && - other.updatedAt == updatedAt && - other.ownerId == ownerId && - other.name == name && - other.faceAssetId == faceAssetId && - other.isFavorite == isFavorite && - other.isHidden == isHidden && - other.color == color && - other.birthDate == birthDate; - } - - @override - int get hashCode { - return id.hashCode ^ - createdAt.hashCode ^ - updatedAt.hashCode ^ - ownerId.hashCode ^ - name.hashCode ^ - faceAssetId.hashCode ^ - isFavorite.hashCode ^ - isHidden.hashCode ^ - color.hashCode ^ - birthDate.hashCode; - } + }) = _DriftPerson; } diff --git a/mobile/lib/domain/models/stack.model.dart b/mobile/lib/domain/models/stack.model.dart index f17f5788c9..4e88a02c6c 100644 --- a/mobile/lib/domain/models/stack.model.dart +++ b/mobile/lib/domain/models/stack.model.dart @@ -1,57 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'stack.model.freezed.dart'; + // Model for a stack stored in the server -class Stack { - final String id; - final DateTime createdAt; - final DateTime updatedAt; - final String ownerId; - final String primaryAssetId; - - const Stack({ - required this.id, - required this.createdAt, - required this.updatedAt, - required this.ownerId, - required this.primaryAssetId, - }); - - Stack copyWith({String? id, DateTime? createdAt, DateTime? updatedAt, String? ownerId, String? primaryAssetId}) { - return Stack( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ownerId: ownerId ?? this.ownerId, - primaryAssetId: primaryAssetId ?? this.primaryAssetId, - ); - } - - @override - String toString() { - return '''Stack { - id: $id, - createdAt: $createdAt, - updatedAt: $updatedAt, - ownerId: $ownerId, - primaryAssetId: $primaryAssetId -}'''; - } - - @override - bool operator ==(covariant Stack other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.createdAt == createdAt && - other.updatedAt == updatedAt && - other.ownerId == ownerId && - other.primaryAssetId == primaryAssetId; - } - - @override - int get hashCode { - return id.hashCode ^ createdAt.hashCode ^ updatedAt.hashCode ^ ownerId.hashCode ^ primaryAssetId.hashCode; - } +@freezed +abstract class Stack with _$Stack { + const factory Stack({ + required String id, + required DateTime createdAt, + required DateTime updatedAt, + required String ownerId, + required String primaryAssetId, + }) = _Stack; } class StackResponse { diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index b73f798255..8bce1aca13 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -1,5 +1,9 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +part 'user_metadata.model.freezed.dart'; + enum UserMetadataKey { // do not change this order! onboarding, @@ -23,7 +27,7 @@ class Onboarding { } factory Onboarding.fromMap(Map map) { - return Onboarding(isOnboarded: map["isOnboarded"] as bool); + return Onboarding(isOnboarded: map["isOnboarded"]! as bool); } @override @@ -195,9 +199,9 @@ class License { factory License.fromMap(Map map) { return License( - activatedAt: DateTime.parse(map["activatedAt"] as String), - activationKey: map["activationKey"] as String, - licenseKey: map["licenseKey"] as String, + activatedAt: DateTime.parse(map["activatedAt"]! as String), + activationKey: map["activationKey"]! as String, + licenseKey: map["licenseKey"]! as String, ); } @@ -224,61 +228,17 @@ licenseKey: $licenseKey, } // Model for a user metadata stored in the server -class UserMetadata { - final String userId; - final UserMetadataKey key; - final Onboarding? onboarding; - final Preferences? preferences; - final License? license; - - const UserMetadata({required this.userId, required this.key, this.onboarding, this.preferences, this.license}) - : assert( - onboarding != null || preferences != null || license != null, - 'One of onboarding, preferences and license must be provided', - ); - - UserMetadata copyWith({ - String? userId, - UserMetadataKey? key, +@freezed +abstract class UserMetadata with _$UserMetadata { + @Assert( + 'onboarding != null || preferences != null || license != null', + 'One of onboarding, preferences and license must be provided', + ) + const factory UserMetadata({ + required String userId, + required UserMetadataKey key, Onboarding? onboarding, Preferences? preferences, License? license, - }) { - return UserMetadata( - userId: userId ?? this.userId, - key: key ?? this.key, - onboarding: onboarding ?? this.onboarding, - preferences: preferences ?? this.preferences, - license: license ?? this.license, - ); - } - - @override - String toString() { - return '''UserMetadata: { -userId: $userId, -key: $key, -onboarding: ${onboarding ?? ""}, -preferences: ${preferences ?? ""}, -license: ${license ?? ""}, -}'''; - } - - @override - bool operator ==(covariant UserMetadata other) { - if (identical(this, other)) { - return true; - } - - return other.userId == userId && - other.key == key && - other.onboarding == onboarding && - other.preferences == preferences && - other.license == license; - } - - @override - int get hashCode { - return userId.hashCode ^ key.hashCode ^ onboarding.hashCode ^ preferences.hashCode ^ license.hashCode; - } + }) = _UserMetadata; } diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index f4c4a519d3..59f41f1e51 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -2,10 +2,15 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/utils/option.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -14,12 +19,16 @@ class AssetService { final RemoteExifRepository _exifRepository; final DriftLocalAssetRepository _localRepository; final AssetApiRepository _apiRepository; + final AssetMediaRepository _mediaRepository; + final DriftTrashedLocalAssetRepository _trashedLocalRepository; const AssetService({ required this._remoteRepository, required this._exifRepository, required this._localRepository, required this._apiRepository, + required this._mediaRepository, + required this._trashedLocalRepository, }); Future getAsset(BaseAsset asset) { @@ -36,8 +45,8 @@ class AssetService { return _localRepository.getByChecksum(checksum); } - Future getRemoteAssetByChecksum(String checksum) { - return _remoteRepository.getByChecksum(checksum); + Future> getAllRemoteAssetDebugByChecksum(String checksum) { + return _remoteRepository.getAllDebugForChecksum(checksum); } Future getRemoteAsset(String id) { @@ -167,17 +176,25 @@ class AssetService { } } - // TODO(shenlong): remove after action migration + Future deleteLocal(List localIds) async { + if (localIds.isEmpty) { + return 0; + } + + final deletedIds = await _mediaRepository.deleteAll(localIds); + if (deletedIds.isEmpty) { + return 0; + } + + if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + await _trashedLocalRepository.applyTrashedAssets(deletedIds); + } else { + await _localRepository.delete(deletedIds); + } + return deletedIds.length; + } + Future getLocalAsset(String id) { return _localRepository.get(id); } - - Future updateFavorite(List remoteIds, bool isFavorite) async { - if (remoteIds.isEmpty) { - return; - } - - await _apiRepository.updateFavorite(remoteIds, isFavorite); - await _remoteRepository.updateFavorite(remoteIds, isFavorite); - } } diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index e4c332b283..8a573ee209 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -32,7 +32,7 @@ class HashService { }) : _batchSize = batchSize ?? kBatchHashFileLimit { // Stop the in-flight native hash call promptly on cancellation; the loops // below also observe [isCancelled] to bail between batches. - _cancellation?.future.then((_) => _nativeSyncApi.cancelHashing().onError(_log.warning)); + unawaited(_cancellation?.future.then((_) => _nativeSyncApi.cancelHashing().onError(_log.warning))); } bool get isCancelled => _cancellation?.isCompleted ?? false; @@ -66,11 +66,12 @@ class HashService { await _hashAssets(pseudoAlbum, trashedToHash, isTrashed: true); } } - } on PlatformException catch (e) { + } on PlatformException catch (e, s) { if (e.code == _kHashCancelledCode) { _log.warning("Hashing cancelled by platform"); return; } + _log.severe("Native hashing failed: ${e.code}", e, s); } catch (e, s) { _log.severe("Error during hashing", e, s); } diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index feb104f90d..b4ebc66f89 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -40,7 +40,7 @@ class LocalSyncService { required this._permissionRepository, this._cancellation, }) { - _cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning)); + unawaited(_cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning))); } bool get _isCancelled => _cancellation?.isCompleted ?? false; diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index c0bbaa8127..e59d75b01a 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -105,10 +105,8 @@ class RemoteAlbumService { switch (filterMode) { case QuickFilterMode.myAlbums: filtered = filtered.where((album) => album.ownerId == userId).toList(); - break; case QuickFilterMode.sharedWithMe: filtered = filtered.where((album) => album.ownerId != userId).toList(); - break; case QuickFilterMode.all: break; } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 9ebce300ba..9b4d4bb275 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -155,7 +155,7 @@ class SyncStreamService { } Future _handleEvents(List events, Function() abort, Function() reset) async { - List items = []; + final List items = []; for (final event in events) { if (isCancelled) { _logger.warning("Sync stream cancelled"); diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 4cc58b0fe7..b20ba306ff 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -106,32 +106,34 @@ class TimelineService { TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) { _bucketSubscription = _bucketSource().listen((buckets) { - _mutex.run(() async { - final totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); + unawaited( + _mutex.run(() async { + final totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); - if (totalAssets == 0) { - _bufferOffset = 0; - _buffer = []; - } else { - final int offset; - final int count; - // When the buffer is empty or the old bufferOffset is greater than the new total assets, - // we need to reset the buffer and load the first batch of assets. - if (_bufferOffset >= totalAssets || _buffer.isEmpty) { - offset = 0; - count = kTimelineAssetLoadBatchSize; + if (totalAssets == 0) { + _bufferOffset = 0; + _buffer = []; } else { - offset = _bufferOffset; - count = math.min(_buffer.length, totalAssets - _bufferOffset); + final int offset; + final int count; + // When the buffer is empty or the old bufferOffset is greater than the new total assets, + // we need to reset the buffer and load the first batch of assets. + if (_bufferOffset >= totalAssets || _buffer.isEmpty) { + offset = 0; + count = kTimelineAssetLoadBatchSize; + } else { + offset = _bufferOffset; + count = math.min(_buffer.length, totalAssets - _bufferOffset); + } + _buffer = await _assetSource(offset, count); + _bufferOffset = offset; } - _buffer = await _assetSource(offset, count); - _bufferOffset = offset; - } - // change the state's total assets count only after the buffer is reloaded - _totalAssets = totalAssets; - EventStream.shared.emit(const TimelineReloadEvent()); - }); + // change the state's total assets count only after the buffer is reloaded + _totalAssets = totalAssets; + EventStream.shared.emit(const TimelineReloadEvent()); + }), + ); }); } @@ -178,7 +180,7 @@ class TimelineService { if (!hasRange(index, count)) { throw RangeError('TimelineService::getAssets Index out of range'); } - int start = index - _bufferOffset; + final int start = index - _bufferOffset; return _buffer.slice(start, start + count); } diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index 82f397d9b6..db0d93c92f 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -28,6 +28,7 @@ class BackgroundSyncManager { final SyncErrorCallback? onCloudIdSyncError; Cancelable? _syncTask; + bool _syncQueued = false; Cancelable? _syncWebsocketTask; Cancelable? _cloudIdSyncTask; Cancelable? _deviceAlbumSyncTask; @@ -50,6 +51,7 @@ class BackgroundSyncManager { }); Future cancel() async { + _syncQueued = false; final tasks = [ _syncTask, _syncWebsocketTask, @@ -133,8 +135,9 @@ class BackgroundSyncManager { }); } - Future syncRemote() { + Future syncRemote({bool enqueue = false}) { if (_syncTask != null) { + _syncQueued |= enqueue; return _syncTask!.future.then((result) => result ?? false).catchError((_) => false); } @@ -148,15 +151,20 @@ class BackgroundSyncManager { .then((result) { final success = result ?? false; onRemoteSyncComplete?.call(success); + _syncQueued &= success; return success; }) .catchError((error) { onRemoteSyncError?.call(error.toString()); - _syncTask = null; + _syncQueued = false; return false; }) .whenComplete(() { _syncTask = null; + if (_syncQueued) { + _syncQueued = false; + unawaited(syncRemote()); + } }); } diff --git a/mobile/lib/domain/utils/event_stream.dart b/mobile/lib/domain/utils/event_stream.dart index 5967fdca50..0069c75f86 100644 --- a/mobile/lib/domain/utils/event_stream.dart +++ b/mobile/lib/domain/utils/event_stream.dart @@ -32,7 +32,7 @@ class EventStream { } /// Closes the stream controller - void dispose() { - _controller.close(); + Future dispose() { + return _controller.close(); } } diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index 52d31cb0b3..ec1aa80ceb 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -15,8 +15,8 @@ extension DTOToAsset on api.AssetResponseDto { ownerId: ownerId, visibility: visibility.toAssetVisibility(), durationMs: duration, - height: height?.toInt(), - width: width?.toInt(), + height: height, + width: width, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId.orElse(null), thumbHash: thumbhash, @@ -38,8 +38,8 @@ extension DTOToAsset on api.AssetResponseDto { ownerId: ownerId, visibility: visibility.toAssetVisibility(), durationMs: duration, - height: height?.toInt(), - width: width?.toInt(), + height: height, + width: width, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId.orElse(null), thumbHash: thumbhash, diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index b861eb0570..53e8e73fd1 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -5,7 +5,8 @@ import 'package:collection/collection.dart'; extension ListExtension on List { List uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) { compare ??= (E a, E b) => a == b ? 0 : 1; - int i = 1, j = 1; + int i = 1; + int j = 1; for (; i < length; i++) { if (compare(this[i - 1], this[i]) != 0) { if (i != j) { diff --git a/mobile/lib/extensions/datetime_extensions.dart b/mobile/lib/extensions/datetime_extensions.dart index 0bc95565a6..6a51d15df0 100644 --- a/mobile/lib/extensions/datetime_extensions.dart +++ b/mobile/lib/extensions/datetime_extensions.dart @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; extension TimeAgoExtension on DateTime { /// Displays the time difference of this [DateTime] object to the current time as a [String] String timeAgo({bool numericDates = true}) { - DateTime date = toLocal(); + final DateTime date = toLocal(); final now = DateTime.now().toLocal(); final difference = now.difference(date); @@ -13,27 +13,27 @@ extension TimeAgoExtension on DateTime { } else if (difference.inSeconds < 60) { return '${difference.inSeconds} seconds ago'; } else if (difference.inMinutes <= 1) { - return (numericDates) ? '1 minute ago' : 'A minute ago'; + return numericDates ? '1 minute ago' : 'A minute ago'; } else if (difference.inMinutes < 60) { return '${difference.inMinutes} minutes ago'; } else if (difference.inHours <= 1) { - return (numericDates) ? '1 hour ago' : 'An hour ago'; + return numericDates ? '1 hour ago' : 'An hour ago'; } else if (difference.inHours < 60) { return '${difference.inHours} hours ago'; } else if (difference.inDays <= 1) { - return (numericDates) ? '1 day ago' : 'Yesterday'; + return numericDates ? '1 day ago' : 'Yesterday'; } else if (difference.inDays < 6) { return '${difference.inDays} days ago'; } else if ((difference.inDays / 7).ceil() <= 1) { - return (numericDates) ? '1 week ago' : 'Last week'; + return numericDates ? '1 week ago' : 'Last week'; } else if ((difference.inDays / 7).ceil() < 4) { return '${(difference.inDays / 7).ceil()} weeks ago'; } else if ((difference.inDays / 30).ceil() <= 1) { - return (numericDates) ? '1 month ago' : 'Last month'; + return numericDates ? '1 month ago' : 'Last month'; } else if ((difference.inDays / 30).ceil() < 30) { return '${(difference.inDays / 30).ceil()} months ago'; } else if ((difference.inDays / 365).ceil() <= 1) { - return (numericDates) ? '1 year ago' : 'Last year'; + return numericDates ? '1 year ago' : 'Last year'; } return '${(difference.inDays / 365).floor()} years ago'; } diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 87a05ab8fe..58e1a91d33 100644 --- a/mobile/lib/infrastructure/entities/asset_edit.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -25,7 +25,7 @@ class AssetEditEntity extends Table with DriftDefaultsMixin { } final JsonTypeConverter2, Uint8List, Object?> editParameterConverter = TypeConverter.jsonb( - fromJson: (json) => json as Map, + fromJson: (json) => json! as Map, ); extension AssetEditEntityDataDomainEx on AssetEditEntityData { diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 120fbd0c68..3328b38f31 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -79,8 +79,8 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { orientation: orientation, latitude: latitude, longitude: longitude, - f: fNumber?.toDouble(), - mm: focalLength?.toDouble(), + f: fNumber, + mm: focalLength, lens: lens, isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), exposureSeconds: ExifDtoConverter.exposureTimeToSeconds(exposureTime), diff --git a/mobile/lib/infrastructure/entities/log.entity.dart b/mobile/lib/infrastructure/entities/log.entity.dart index e578459827..264bd3331f 100644 --- a/mobile/lib/infrastructure/entities/log.entity.dart +++ b/mobile/lib/infrastructure/entities/log.entity.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart'; import 'package:immich_mobile/domain/models/log.model.dart' as domain; +import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart'; class LogMessageEntity extends Table { const LogMessageEntity(); diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index ede3de3966..da2070fd71 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -17,5 +17,5 @@ class UserMetadataEntity extends Table with DriftDefaultsMixin { } final JsonTypeConverter2, Uint8List, Object?> userMetadataConverter = TypeConverter.jsonb( - fromJson: (json) => json as Map, + fromJson: (json) => json! as Map, ); diff --git a/mobile/lib/infrastructure/loaders/image_request.dart b/mobile/lib/infrastructure/loaders/image_request.dart index d0f3679084..8b7cdc7621 100644 --- a/mobile/lib/infrastructure/loaders/image_request.dart +++ b/mobile/lib/infrastructure/loaders/image_request.dart @@ -18,7 +18,7 @@ abstract class ImageRequest { final int requestId = _nextRequestId++; bool _isCancelled = false; - get isCancelled => _isCancelled; + bool get isCancelled => _isCancelled; ImageRequest(); diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index 40705c16d2..d6a25753fb 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -14,8 +14,13 @@ class RemoteImageRequest extends ImageRequest { final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false); // Android falls back to encoded data if native decoding fails, so check for both shapes of the response. final frame = switch (info) { - {'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length), - {'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} => + {'pointer': final int pointer, 'length': final int length} => await _fromEncodedPlatformImage(pointer, length), + { + 'pointer': final int pointer, + 'width': final int width, + 'height': final int height, + 'rowBytes': final int rowBytes, + } => await _fromDecodedPlatformImage(pointer, width, height, rowBytes), _ => null, }; diff --git a/mobile/lib/infrastructure/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart index 0241711d4b..eb3cba95d0 100644 --- a/mobile/lib/infrastructure/repositories/backup.repository.dart +++ b/mobile/lib/infrastructure/repositories/backup.repository.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -16,7 +17,7 @@ class DriftBackupRepository extends DriftDatabaseRepository { final Drift _db; const DriftBackupRepository(this._db) : super(_db); - _getExcludedSubquery() { + JoinedSelectStatement<$LocalAlbumAssetEntityTable, LocalAlbumAssetEntityData> _getExcludedSubquery() { return _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) ..join([ diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index a443ffb975..a9911fe044 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -356,7 +356,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return _deleteAssets(assetIds); } - List assetsToDelete = []; + final List assetsToDelete = []; List assetsToUnLink = []; final uniqueAssets = await _getUniqueAssetsInAlbum(albumId); diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index c34d2c4697..8396d6d2a6 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -24,13 +24,21 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { const DriftLocalAssetRepository(this._db) : super(_db); SingleOrNullSelectable _assetSelectable(String id) { - final query = _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([ - leftOuterJoin( - _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), - useColumns: false, - ), - ])..where(_db.localAssetEntity.id.equals(id)); + final query = + _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([ + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) & + _db.remoteAssetEntity.ownerId.isInQuery( + _db.selectOnly(_db.authUserEntity) + ..addColumns([_db.authUserEntity.id]) + ..limit(1), + ), + useColumns: false, + ), + ]) + ..where(_db.localAssetEntity.id.equals(id)) + ..limit(1); return query.map((row) { final asset = row.readTable(_db.localAssetEntity).toDto(); diff --git a/mobile/lib/infrastructure/repositories/ocr.repository.dart b/mobile/lib/infrastructure/repositories/ocr.repository.dart index 99f4fa4675..c3465df357 100644 --- a/mobile/lib/infrastructure/repositories/ocr.repository.dart +++ b/mobile/lib/infrastructure/repositories/ocr.repository.dart @@ -1,7 +1,7 @@ +import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/ocr.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:drift/drift.dart'; class OcrRepository extends DriftDatabaseRepository { final Drift _db; diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index cdf8bfa15b..d18284b636 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -59,10 +59,10 @@ class RemoteAssetRepository extends DriftDatabaseRepository { return _assetSelectable(id).getSingleOrNull(); } - Future getByChecksum(String checksum) { + Future> getAllDebugForChecksum(String checksum) { final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum)); - return query.map((row) => row.toDto()).getSingleOrNull(); + return query.map((row) => row.toDto()).get(); } Future> getStackChildren(RemoteAsset asset) { @@ -117,18 +117,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository { }).get(); } - Future updateFavorite(List ids, bool isFavorite) { - return _db.batch((batch) async { - for (final id in ids) { - batch.update( - _db.remoteAssetEntity, - RemoteAssetEntityCompanion(isFavorite: Value(isFavorite)), - where: (e) => e.id.equals(id), - ); - } - }); - } - Future updateVisibility(List ids, AssetVisibility visibility) { return _db.batch((batch) async { for (final id in ids) { diff --git a/mobile/lib/infrastructure/repositories/search_api.repository.dart b/mobile/lib/infrastructure/repositories/search_api.repository.dart index 395d4045cf..dec6e1d167 100644 --- a/mobile/lib/infrastructure/repositories/search_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/search_api.repository.dart @@ -21,28 +21,28 @@ class SearchApiRepository extends ApiRepository { (filter.assetId != null && filter.assetId!.isNotEmpty)) { return _api.searchSmart( SmartSearchDto( - query: filter.context == null ? const Optional.absent() : Optional.present(filter.context!), - queryAssetId: filter.assetId == null ? const Optional.absent() : Optional.present(filter.assetId!), - language: filter.language == null ? const Optional.absent() : Optional.present(filter.language!), + query: filter.context == null ? const Optional.absent() : Optional.present(filter.context), + queryAssetId: filter.assetId == null ? const Optional.absent() : Optional.present(filter.assetId), + language: filter.language == null ? const Optional.absent() : Optional.present(filter.language), country: filter.location.country == null ? const Optional.absent() - : Optional.present(filter.location.country!), - state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!), - city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!), - make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!), - model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!), + : Optional.present(filter.location.country), + state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state), + city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city), + make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make), + model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model), takenAfter: filter.date.takenAfter == null ? const Optional.absent() - : Optional.present(filter.date.takenAfter!), + : Optional.present(filter.date.takenAfter), takenBefore: filter.date.takenBefore == null ? const Optional.absent() - : Optional.present(filter.date.takenBefore!), + : Optional.present(filter.date.takenBefore), visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline), rating: filter.rating.rating.toOptional(), isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(), isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(), personIds: Optional.present(filter.people.map((e) => e.id).toList()), - tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!), + tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds), type: type == null ? const Optional.absent() : Optional.present(type), page: Optional.present(page), size: const Optional.present(100), @@ -53,29 +53,27 @@ class SearchApiRepository extends ApiRepository { return _api.searchAssets( MetadataSearchDto( originalFileName: filter.filename != null && filter.filename!.isNotEmpty - ? Optional.present(filter.filename!) + ? Optional.present(filter.filename) : const Optional.absent(), - country: filter.location.country == null ? const Optional.absent() : Optional.present(filter.location.country!), + country: filter.location.country == null ? const Optional.absent() : Optional.present(filter.location.country), description: filter.description != null && filter.description!.isNotEmpty - ? Optional.present(filter.description!) + ? Optional.present(filter.description) : const Optional.absent(), - ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? Optional.present(filter.ocr!) : const Optional.absent(), - state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!), - city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!), - make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!), - model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!), - takenAfter: filter.date.takenAfter == null - ? const Optional.absent() - : Optional.present(filter.date.takenAfter!), + ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? Optional.present(filter.ocr) : const Optional.absent(), + state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state), + city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city), + make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make), + model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model), + takenAfter: filter.date.takenAfter == null ? const Optional.absent() : Optional.present(filter.date.takenAfter), takenBefore: filter.date.takenBefore == null ? const Optional.absent() - : Optional.present(filter.date.takenBefore!), + : Optional.present(filter.date.takenBefore), visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline), rating: filter.rating.rating.toOptional(), isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(), isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(), personIds: Optional.present(filter.people.map((e) => e.id).toList()), - tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!), + tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds), type: type == null ? const Optional.absent() : Optional.present(type), page: Optional.present(page), size: const Optional.present(1000), diff --git a/mobile/lib/infrastructure/repositories/settings.repository.dart b/mobile/lib/infrastructure/repositories/settings.repository.dart index 7063779336..c5f8fb48d8 100644 --- a/mobile/lib/infrastructure/repositories/settings.repository.dart +++ b/mobile/lib/infrastructure/repositories/settings.repository.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; @@ -29,6 +30,15 @@ class SettingsRepository extends CachedKeyValueRepository reset() async { + final instance = _instance; + if (instance != null) { + await instance.clear(SettingsKey.values); + _instance = null; + } + } + @override List get keys => SettingsKey.values; diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 3a63812485..9500190abc 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_slow_async_io + import 'dart:io'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index e9d57f7506..303859da2f 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -80,7 +80,7 @@ class SyncApiRepository { ); String previousChunk = ''; - List lines = []; + final List lines = []; bool shouldAbort = false; @@ -105,7 +105,7 @@ class SyncApiRepository { } previousChunk += chunk; - final parts = previousChunk.toString().split('\n'); + final parts = previousChunk.split('\n'); previousChunk = parts.removeLast(); lines.addAll(parts); diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index bdfb1942ab..c43de69c5d 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -31,8 +31,8 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction; -import 'package:openapi/api.dart' hide UserMetadataKey, AssetEditAction, AssetVisibility, AlbumUserRole; +import 'package:openapi/api.dart' as api show AlbumUserRole, AssetEditAction, AssetVisibility, UserMetadataKey; +import 'package:openapi/api.dart' hide AlbumUserRole, AssetEditAction, AssetVisibility, UserMetadataKey; class SyncStreamRepository extends DriftDatabaseRepository { final Logger _logger = Logger('DriftSyncStreamRepository'); @@ -287,8 +287,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { fNumber: Value(exif.fNumber), fileSize: Value(exif.fileSizeInByte), focalLength: Value(exif.focalLength), - latitude: Value(exif.latitude?.toDouble()), - longitude: Value(exif.longitude?.toDouble()), + latitude: Value(exif.latitude), + longitude: Value(exif.longitude), iso: Value(exif.iso), make: Value(exif.make), model: Value(exif.model), @@ -358,12 +358,12 @@ class SyncStreamRepository extends DriftDatabaseRepository { final map = metadata.value as Map; final companion = RemoteAssetCloudIdEntityCompanion( cloudId: Value(map['iCloudId']?.toString()), - createdAt: Value(map['createdAt'] != null ? DateTime.parse(map['createdAt'] as String) : null), + createdAt: Value(map['createdAt'] != null ? DateTime.parse(map['createdAt']! as String) : null), adjustmentTime: Value( - map['adjustmentTime'] != null ? DateTime.parse(map['adjustmentTime'] as String) : null, + map['adjustmentTime'] != null ? DateTime.parse(map['adjustmentTime']! as String) : null, ), - latitude: Value(map['latitude'] != null ? (double.tryParse(map['latitude'] as String)) : null), - longitude: Value(map['longitude'] != null ? (double.tryParse(map['longitude'] as String)) : null), + latitude: Value(map['latitude'] != null ? (double.tryParse(map['latitude']! as String)) : null), + longitude: Value(map['longitude'] != null ? (double.tryParse(map['longitude']! as String)) : null), ); batch.insert( _db.remoteAssetCloudIdEntity, diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 6a9fbdb204..82ad38c80f 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -139,11 +139,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), useColumns: false, ), - leftOuterJoin( - _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), - useColumns: false, - ), ]) ..addColumns([assetCountExp, dateExp]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) @@ -167,7 +162,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), leftOuterJoin( _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) & + _db.remoteAssetEntity.ownerId.isInQuery( + _db.selectOnly(_db.authUserEntity) + ..addColumns([_db.authUserEntity.id]) + ..limit(1), + ), useColumns: false, ), ]) diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index 08712588d9..0f50776e4e 100644 --- a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart @@ -16,6 +16,11 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { const DriftTrashedLocalAssetRepository(this._db) : super(_db); + /// Matches remote_asset_entity rows owned by the current user. The asset is unique over (owner, checksum), + /// so partners can have a duplicate checksum + Expression get _ownedByCurrentUser => + _db.remoteAssetEntity.ownerId.isInQuery(_db.selectOnly(_db.authUserEntity)..addColumns([_db.authUserEntity.id])); + Future updateHashes(Map hashes) { if (hashes.isEmpty) { return Future.value(); @@ -45,7 +50,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { await (_db.select(_db.trashedLocalAssetEntity).join([ innerJoin( _db.remoteAssetEntity, - _db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum), + _db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum) & _ownedByCurrentUser, ), ])..where( _db.trashedLocalAssetEntity.source.equalsValue(TrashOrigin.remoteSync) & @@ -66,7 +71,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { return; } final assetIds = trashedAssets.map((e) => e.asset.id).toSet(); - Map localChecksumById = await _getCachedChecksums(assetIds); + final Map localChecksumById = await _getCachedChecksums(assetIds); return _db.transaction(() async { await _db.batch((batch) { @@ -273,7 +278,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), leftOuterJoin( _db.remoteAssetEntity, - _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum) & _ownedByCurrentUser, ), ])..where( _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & diff --git a/mobile/lib/infrastructure/utils/exif.converter.dart b/mobile/lib/infrastructure/utils/exif.converter.dart index 9f9b6f9324..d47c9cc97a 100644 --- a/mobile/lib/infrastructure/utils/exif.converter.dart +++ b/mobile/lib/infrastructure/utils/exif.converter.dart @@ -21,7 +21,7 @@ abstract final class ExifDtoConverter { lens: dto.lensModel.orElse(null), f: dto.fNumber.orElse(null)?.toDouble(), mm: dto.focalLength.orElse(null)?.toDouble(), - iso: dto.iso.orElse(null)?.toInt(), + iso: dto.iso.orElse(null), exposureSeconds: exposureTimeToSeconds(dto.exposureTime.orElse(null)), ); } @@ -40,7 +40,7 @@ abstract final class ExifDtoConverter { if (second == null) { return null; } - double? value = double.tryParse(second); + final double? value = double.tryParse(second); if (value != null) { return value; } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index f576a7c63c..317733f1de 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -24,13 +24,13 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; @@ -84,7 +84,7 @@ Future initApp() async { FlutterError.presentError(details); log.severe( 'FlutterError - Catch all', - "${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}", + "$details\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}", details.stack, ); }; @@ -128,44 +128,28 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve switch (state) { case AppLifecycleState.resumed: dPrint(() => "[APP STATE] resumed"); - ref.read(appStateProvider.notifier).handleAppResume(); + unawaited(ref.read(appStateProvider.notifier).handleAppResume()); unawaited(ref.read(viewIntentHandlerProvider).onAppResumed()); - break; case AppLifecycleState.inactive: dPrint(() => "[APP STATE] inactive"); ref.read(appStateProvider.notifier).handleAppInactivity(); - break; case AppLifecycleState.paused: dPrint(() => "[APP STATE] paused"); - ref.read(appStateProvider.notifier).handleAppPause(); - break; + unawaited(ref.read(appStateProvider.notifier).handleAppPause()); case AppLifecycleState.detached: dPrint(() => "[APP STATE] detached"); - ref.read(appStateProvider.notifier).handleAppDetached(); - break; + unawaited(ref.read(appStateProvider.notifier).handleAppDetached()); case AppLifecycleState.hidden: dPrint(() => "[APP STATE] hidden"); ref.read(appStateProvider.notifier).handleAppHidden(); - break; } } Future initApp() async { WidgetsBinding.instance.addObserver(this); - // Draw the app from edge to edge unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); - - // Sets the navigation bar color - SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent); - if (Platform.isAndroid) { - // Android 8 does not support transparent app bars - final info = await DeviceInfoPlugin().androidInfo; - if (info.version.sdkInt <= 26) { - overlayStyle = context.isDarkTheme ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light; - } - } - SystemChrome.setSystemUIOverlayStyle(overlayStyle); + await _setNavigationBarColor(); await FlutterLocalNotificationsPlugin().initialize( const InitializationSettings( @@ -175,6 +159,22 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve ); } + Future _setNavigationBarColor() async { + SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent); + if (Platform.isAndroid) { + // Android 8 does not support transparent app bars + final info = await DeviceInfoPlugin().androidInfo; + if (!mounted) { + return; + } + + if (info.version.sdkInt <= 26) { + overlayStyle = context.isDarkTheme ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light; + } + } + SystemChrome.setSystemUIOverlayStyle(overlayStyle); + } + Future _deepLinkBuilder(PlatformDeepLink deepLink) async { final deepLinkHandler = ref.read(deepLinkServiceProvider); final currentRouteName = ref.read(currentRouteNameProvider.notifier).state; @@ -219,19 +219,21 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve } @override - initState() { + void initState() { super.initState(); - initApp().then((_) => dPrint(() => "App Init Completed")); + unawaited(initApp().then((_) => dPrint(() => "App Init Completed"))); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - ref.read(backgroundWorkerFgServiceProvider).enable(); + unawaited(ref.read(backgroundWorkerFgServiceProvider).enable()); if (Platform.isAndroid) { - ref - .read(backgroundWorkerFgServiceProvider) - .saveNotificationMessage( - StaticTranslations.instance.uploading_media, - StaticTranslations.instance.backup_background_service_default_notification, - ); + unawaited( + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, + ), + ); } }); @@ -248,7 +250,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve @override void reassemble() { if (kDebugMode) { - NetworkRepository.init(); + unawaited(NetworkRepository.init()); } super.reassemble(); } diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart index d3f99aea64..9a589ef729 100644 --- a/mobile/lib/models/activities/activity.model.dart +++ b/mobile/lib/models/activities/activity.model.dart @@ -1,65 +1,21 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +part 'activity.model.freezed.dart'; + enum ActivityType { comment, like } -class Activity { - final String id; - final String? assetId; - final String? comment; - final DateTime createdAt; - final ActivityType type; - final UserDto user; - - const Activity({ - required this.id, - this.assetId, - this.comment, - required this.createdAt, - required this.type, - required this.user, - }); - - Activity copyWith({ - String? id, +@freezed +abstract class Activity with _$Activity { + const factory Activity({ + required String id, String? assetId, String? comment, - DateTime? createdAt, - ActivityType? type, - UserDto? user, - }) { - return Activity( - id: id ?? this.id, - assetId: assetId ?? this.assetId, - comment: comment ?? this.comment, - createdAt: createdAt ?? this.createdAt, - type: type ?? this.type, - user: user ?? this.user, - ); - } - - @override - String toString() { - return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)'; - } - - @override - bool operator ==(covariant Activity other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.assetId == assetId && - other.comment == comment && - other.createdAt == createdAt && - other.type == type && - other.user == user; - } - - @override - int get hashCode { - return id.hashCode ^ assetId.hashCode ^ comment.hashCode ^ createdAt.hashCode ^ type.hashCode ^ user.hashCode; - } + required DateTime createdAt, + required ActivityType type, + required UserDto user, + }) = _Activity; } class ActivityStats { diff --git a/mobile/lib/models/auth/auth_state.model.dart b/mobile/lib/models/auth/auth_state.model.dart index c8a8018929..0a3bf983b2 100644 --- a/mobile/lib/models/auth/auth_state.model.dart +++ b/mobile/lib/models/auth/auth_state.model.dart @@ -1,71 +1,17 @@ -class AuthState { - final String deviceId; - final String userId; - final String userEmail; - final bool isAuthenticated; - final String name; - final bool isAdmin; - final String profileImagePath; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; - const AuthState({ - required this.deviceId, - required this.userId, - required this.userEmail, - required this.isAuthenticated, - required this.name, - required this.isAdmin, - required this.profileImagePath, - }); +part 'auth_state.model.freezed.dart'; - AuthState copyWith({ - String? deviceId, - String? userId, - String? userEmail, - bool? isAuthenticated, - String? name, - bool? isAdmin, - String? profileImagePath, - }) { - return AuthState( - deviceId: deviceId ?? this.deviceId, - userId: userId ?? this.userId, - userEmail: userEmail ?? this.userEmail, - isAuthenticated: isAuthenticated ?? this.isAuthenticated, - name: name ?? this.name, - isAdmin: isAdmin ?? this.isAdmin, - profileImagePath: profileImagePath ?? this.profileImagePath, - ); - } - - @override - String toString() { - return 'AuthenticationState(deviceId: $deviceId, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, name: $name, isAdmin: $isAdmin, profileImagePath: $profileImagePath)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is AuthState && - other.deviceId == deviceId && - other.userId == userId && - other.userEmail == userEmail && - other.isAuthenticated == isAuthenticated && - other.name == name && - other.isAdmin == isAdmin && - other.profileImagePath == profileImagePath; - } - - @override - int get hashCode { - return deviceId.hashCode ^ - userId.hashCode ^ - userEmail.hashCode ^ - isAuthenticated.hashCode ^ - name.hashCode ^ - isAdmin.hashCode ^ - profileImagePath.hashCode; - } +@freezed +abstract class AuthState with _$AuthState { + const factory AuthState({ + required String deviceId, + required String userId, + required String userEmail, + required bool isAuthenticated, + required String name, + required bool isAdmin, + required String profileImagePath, + }) = _AuthState; } diff --git a/mobile/lib/models/map/map_marker.model.dart b/mobile/lib/models/map/map_marker.model.dart index d730f9bd6d..4093747e84 100644 --- a/mobile/lib/models/map/map_marker.model.dart +++ b/mobile/lib/models/map/map_marker.model.dart @@ -1,29 +1,14 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:openapi/api.dart'; -class MapMarker { - final LatLng latLng; - final String assetRemoteId; - const MapMarker({required this.latLng, required this.assetRemoteId}); +part 'map_marker.model.freezed.dart'; - MapMarker copyWith({LatLng? latLng, String? assetRemoteId}) { - return MapMarker(latLng: latLng ?? this.latLng, assetRemoteId: assetRemoteId ?? this.assetRemoteId); - } +@freezed +abstract class MapMarker with _$MapMarker { + const factory MapMarker({required LatLng latLng, required String assetRemoteId}) = _MapMarker; - MapMarker.fromDto(MapMarkerResponseDto dto) : latLng = LatLng(dto.lat, dto.lon), assetRemoteId = dto.id; - - @override - String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)'; - - @override - bool operator ==(covariant MapMarker other) { - if (identical(this, other)) { - return true; - } - - return other.latLng == latLng && other.assetRemoteId == assetRemoteId; - } - - @override - int get hashCode => latLng.hashCode ^ assetRemoteId.hashCode; + factory MapMarker.fromDto(MapMarkerResponseDto dto) => + MapMarker(latLng: LatLng(dto.lat, dto.lon), assetRemoteId: dto.id); } diff --git a/mobile/lib/models/map/map_state.model.dart b/mobile/lib/models/map/map_state.model.dart index a4863aa465..f7bd435073 100644 --- a/mobile/lib/models/map/map_state.model.dart +++ b/mobile/lib/models/map/map_state.model.dart @@ -1,79 +1,20 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class MapState { - final ThemeMode themeMode; - final bool showFavoriteOnly; - final bool includeArchived; - final bool withPartners; - final int relativeTime; - final bool shouldRefetchMarkers; - final AsyncValue lightStyleFetched; - final AsyncValue darkStyleFetched; +part 'map_state.model.freezed.dart'; - const MapState({ - this.themeMode = ThemeMode.system, - this.showFavoriteOnly = false, - this.includeArchived = false, - this.withPartners = false, - this.relativeTime = 0, - this.shouldRefetchMarkers = false, - this.lightStyleFetched = const AsyncLoading(), - this.darkStyleFetched = const AsyncLoading(), - }); - - MapState copyWith({ - ThemeMode? themeMode, - bool? showFavoriteOnly, - bool? includeArchived, - bool? withPartners, - int? relativeTime, - bool? shouldRefetchMarkers, - AsyncValue? lightStyleFetched, - AsyncValue? darkStyleFetched, - }) { - return MapState( - themeMode: themeMode ?? this.themeMode, - showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly, - includeArchived: includeArchived ?? this.includeArchived, - withPartners: withPartners ?? this.withPartners, - relativeTime: relativeTime ?? this.relativeTime, - shouldRefetchMarkers: shouldRefetchMarkers ?? this.shouldRefetchMarkers, - lightStyleFetched: lightStyleFetched ?? this.lightStyleFetched, - darkStyleFetched: darkStyleFetched ?? this.darkStyleFetched, - ); - } - - @override - String toString() { - return 'MapState(themeMode: $themeMode, showFavoriteOnly: $showFavoriteOnly, includeArchived: $includeArchived, withPartners: $withPartners, relativeTime: $relativeTime, shouldRefetchMarkers: $shouldRefetchMarkers, lightStyleFetched: $lightStyleFetched, darkStyleFetched: $darkStyleFetched)'; - } - - @override - bool operator ==(covariant MapState other) { - if (identical(this, other)) { - return true; - } - - return other.themeMode == themeMode && - other.showFavoriteOnly == showFavoriteOnly && - other.includeArchived == includeArchived && - other.withPartners == withPartners && - other.relativeTime == relativeTime && - other.shouldRefetchMarkers == shouldRefetchMarkers && - other.lightStyleFetched == lightStyleFetched && - other.darkStyleFetched == darkStyleFetched; - } - - @override - int get hashCode { - return themeMode.hashCode ^ - showFavoriteOnly.hashCode ^ - includeArchived.hashCode ^ - withPartners.hashCode ^ - relativeTime.hashCode ^ - shouldRefetchMarkers.hashCode ^ - lightStyleFetched.hashCode ^ - darkStyleFetched.hashCode; - } +@freezed +abstract class MapState with _$MapState { + const factory MapState({ + @Default(ThemeMode.system) ThemeMode themeMode, + @Default(false) bool showFavoriteOnly, + @Default(false) bool includeArchived, + @Default(false) bool withPartners, + @Default(0) int relativeTime, + @Default(false) bool shouldRefetchMarkers, + @Default(AsyncLoading()) AsyncValue lightStyleFetched, + @Default(AsyncLoading()) AsyncValue darkStyleFetched, + }) = _MapState; } diff --git a/mobile/lib/models/server_info/server_disk_info.model.dart b/mobile/lib/models/server_info/server_disk_info.model.dart index 16e58b331d..910f06b065 100644 --- a/mobile/lib/models/server_info/server_disk_info.model.dart +++ b/mobile/lib/models/server_info/server_disk_info.model.dart @@ -1,53 +1,22 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:openapi/api.dart'; -class ServerDiskInfo { - final String diskAvailable; - final String diskSize; - final String diskUse; - final double diskUsagePercentage; +part 'server_disk_info.model.freezed.dart'; - const ServerDiskInfo({ - required this.diskAvailable, - required this.diskSize, - required this.diskUse, - required this.diskUsagePercentage, - }); +@freezed +abstract class ServerDiskInfo with _$ServerDiskInfo { + const factory ServerDiskInfo({ + required String diskAvailable, + required String diskSize, + required String diskUse, + required double diskUsagePercentage, + }) = _ServerDiskInfo; - ServerDiskInfo copyWith({String? diskAvailable, String? diskSize, String? diskUse, double? diskUsagePercentage}) { - return ServerDiskInfo( - diskAvailable: diskAvailable ?? this.diskAvailable, - diskSize: diskSize ?? this.diskSize, - diskUse: diskUse ?? this.diskUse, - diskUsagePercentage: diskUsagePercentage ?? this.diskUsagePercentage, - ); - } - - @override - String toString() { - return 'ServerDiskInfo(diskAvailable: $diskAvailable, diskSize: $diskSize, diskUse: $diskUse, diskUsagePercentage: $diskUsagePercentage)'; - } - - ServerDiskInfo.fromDto(ServerStorageResponseDto dto) - : diskAvailable = dto.diskAvailable, - diskSize = dto.diskSize, - diskUse = dto.diskUse, - diskUsagePercentage = dto.diskUsagePercentage; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is ServerDiskInfo && - other.diskAvailable == diskAvailable && - other.diskSize == diskSize && - other.diskUse == diskUse && - other.diskUsagePercentage == diskUsagePercentage; - } - - @override - int get hashCode { - return diskAvailable.hashCode ^ diskSize.hashCode ^ diskUse.hashCode ^ diskUsagePercentage.hashCode; - } + factory ServerDiskInfo.fromDto(ServerStorageResponseDto dto) => ServerDiskInfo( + diskAvailable: dto.diskAvailable, + diskSize: dto.diskSize, + diskUse: dto.diskUse, + diskUsagePercentage: dto.diskUsagePercentage, + ); } diff --git a/mobile/lib/models/server_info/server_features.model.dart b/mobile/lib/models/server_info/server_features.model.dart index c288c1bfbf..4de7e2e3cd 100644 --- a/mobile/lib/models/server_info/server_features.model.dart +++ b/mobile/lib/models/server_info/server_features.model.dart @@ -1,74 +1,26 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:openapi/api.dart'; -class ServerFeatures { - final bool trash; - final bool map; - final bool oauthEnabled; - final bool passwordLogin; - final bool ocr; - final bool smartSearch; +part 'server_features.model.freezed.dart'; - const ServerFeatures({ - required this.trash, - required this.map, - required this.oauthEnabled, - required this.passwordLogin, - this.ocr = false, - this.smartSearch = false, - }); +@freezed +abstract class ServerFeatures with _$ServerFeatures { + const factory ServerFeatures({ + required bool trash, + required bool map, + required bool oauthEnabled, + required bool passwordLogin, + @Default(false) bool ocr, + @Default(false) bool smartSearch, + }) = _ServerFeatures; - ServerFeatures copyWith({ - bool? trash, - bool? map, - bool? oauthEnabled, - bool? passwordLogin, - bool? ocr, - bool? smartSearch, - }) { - return ServerFeatures( - trash: trash ?? this.trash, - map: map ?? this.map, - oauthEnabled: oauthEnabled ?? this.oauthEnabled, - passwordLogin: passwordLogin ?? this.passwordLogin, - ocr: ocr ?? this.ocr, - smartSearch: smartSearch ?? this.smartSearch, - ); - } - - @override - String toString() { - return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch)'; - } - - ServerFeatures.fromDto(ServerFeaturesDto dto) - : trash = dto.trash, - map = dto.map, - oauthEnabled = dto.oauth, - passwordLogin = dto.passwordLogin, - ocr = dto.ocr, - smartSearch = dto.smartSearch; - - @override - bool operator ==(covariant ServerFeatures other) { - if (identical(this, other)) { - return true; - } - - return other.trash == trash && - other.map == map && - other.oauthEnabled == oauthEnabled && - other.passwordLogin == passwordLogin && - other.ocr == ocr && - other.smartSearch == smartSearch; - } - - @override - int get hashCode { - return trash.hashCode ^ - map.hashCode ^ - oauthEnabled.hashCode ^ - passwordLogin.hashCode ^ - ocr.hashCode ^ - smartSearch.hashCode; - } + factory ServerFeatures.fromDto(ServerFeaturesDto dto) => ServerFeatures( + trash: dto.trash, + map: dto.map, + oauthEnabled: dto.oauth, + passwordLogin: dto.passwordLogin, + ocr: dto.ocr, + smartSearch: dto.smartSearch, + ); } diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index 33d6393e15..adc55d3f4e 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -1,9 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/models/server_info/server_features.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; +part 'server_info.model.freezed.dart'; + enum VersionStatus { upToDate, clientOutOfDate, @@ -18,68 +22,14 @@ enum VersionStatus { }; } -class ServerInfo { - final ServerVersion serverVersion; - final ServerVersion? latestVersion; - final ServerFeatures serverFeatures; - final ServerConfig serverConfig; - final ServerDiskInfo serverDiskInfo; - final VersionStatus versionStatus; - - const ServerInfo({ - required this.serverVersion, - this.latestVersion, - required this.serverFeatures, - required this.serverConfig, - required this.serverDiskInfo, - required this.versionStatus, - }); - - ServerInfo copyWith({ - ServerVersion? serverVersion, +@freezed +abstract class ServerInfo with _$ServerInfo { + const factory ServerInfo({ + required ServerVersion serverVersion, ServerVersion? latestVersion, - ServerFeatures? serverFeatures, - ServerConfig? serverConfig, - ServerDiskInfo? serverDiskInfo, - VersionStatus? versionStatus, - }) { - return ServerInfo( - serverVersion: serverVersion ?? this.serverVersion, - latestVersion: latestVersion ?? this.latestVersion, - serverFeatures: serverFeatures ?? this.serverFeatures, - serverConfig: serverConfig ?? this.serverConfig, - serverDiskInfo: serverDiskInfo ?? this.serverDiskInfo, - versionStatus: versionStatus ?? this.versionStatus, - ); - } - - @override - String toString() { - return 'ServerInfo(serverVersion: $serverVersion, latestVersion: $latestVersion, serverFeatures: $serverFeatures, serverConfig: $serverConfig, serverDiskInfo: $serverDiskInfo, versionStatus: $versionStatus)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is ServerInfo && - other.serverVersion == serverVersion && - other.latestVersion == latestVersion && - other.serverFeatures == serverFeatures && - other.serverConfig == serverConfig && - other.serverDiskInfo == serverDiskInfo && - other.versionStatus == versionStatus; - } - - @override - int get hashCode { - return serverVersion.hashCode ^ - latestVersion.hashCode ^ - serverFeatures.hashCode ^ - serverConfig.hashCode ^ - serverDiskInfo.hashCode ^ - versionStatus.hashCode; - } + required ServerFeatures serverFeatures, + required ServerConfig serverConfig, + required ServerDiskInfo serverDiskInfo, + required VersionStatus versionStatus, + }) = _ServerInfo; } diff --git a/mobile/lib/models/shared_link/shared_link.model.dart b/mobile/lib/models/shared_link/shared_link.model.dart index e7b65a96ef..cf96609d3c 100644 --- a/mobile/lib/models/shared_link/shared_link.model.dart +++ b/mobile/lib/models/shared_link/shared_link.model.dart @@ -1,119 +1,47 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:openapi/api.dart'; +part 'shared_link.model.freezed.dart'; + enum SharedLinkSource { album, individual } -class SharedLink { - final String id; - final String title; - final bool allowDownload; - final bool allowUpload; - final String? thumbAssetId; - final String? description; - final String? password; - final DateTime? expiresAt; - final String key; - final bool showMetadata; - final SharedLinkSource type; - final String? slug; +@freezed +abstract class SharedLink with _$SharedLink { + const factory SharedLink({ + required String id, + required String title, + required bool allowDownload, + required bool allowUpload, + required String? thumbAssetId, + required String? description, + required String? password, + required DateTime? expiresAt, + required String key, + required bool showMetadata, + required SharedLinkSource type, + required String? slug, + }) = _SharedLink; - const SharedLink({ - required this.id, - required this.title, - required this.allowDownload, - required this.allowUpload, - required this.thumbAssetId, - required this.description, - required this.password, - required this.expiresAt, - required this.key, - required this.showMetadata, - required this.type, - required this.slug, - }); - - SharedLink copyWith({ - String? id, - String? title, - String? thumbAssetId, - bool? allowDownload, - bool? allowUpload, - String? description, - String? password, - DateTime? expiresAt, - String? key, - bool? showMetadata, - SharedLinkSource? type, - String? slug, - }) { + factory SharedLink.fromDto(SharedLinkResponseDto dto) { + final isAlbum = dto.type == SharedLinkType.ALBUM; return SharedLink( - id: id ?? this.id, - title: title ?? this.title, - thumbAssetId: thumbAssetId ?? this.thumbAssetId, - allowDownload: allowDownload ?? this.allowDownload, - allowUpload: allowUpload ?? this.allowUpload, - description: description ?? this.description, - password: password ?? this.password, - expiresAt: expiresAt ?? this.expiresAt, - key: key ?? this.key, - showMetadata: showMetadata ?? this.showMetadata, - type: type ?? this.type, - slug: slug ?? this.slug, - ); - } - - SharedLink.fromDto(SharedLinkResponseDto dto) - : id = dto.id, - allowDownload = dto.allowDownload, - allowUpload = dto.allowUpload, - description = dto.description, - password = dto.password, - expiresAt = dto.expiresAt, - key = dto.key, - showMetadata = dto.showMetadata, - slug = dto.slug, - type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual, - title = dto.type == SharedLinkType.ALBUM - ? dto.album.orElse(null)?.albumName.toUpperCase() ?? "UNKNOWN SHARE" - : "INDIVIDUAL SHARE", - thumbAssetId = dto.type == SharedLinkType.ALBUM + id: dto.id, + allowDownload: dto.allowDownload, + allowUpload: dto.allowUpload, + description: dto.description, + password: dto.password, + expiresAt: dto.expiresAt, + key: dto.key, + showMetadata: dto.showMetadata, + slug: dto.slug, + type: isAlbum ? SharedLinkSource.album : SharedLinkSource.individual, + title: isAlbum ? dto.album.orElse(null)?.albumName.toUpperCase() ?? "UNKNOWN SHARE" : "INDIVIDUAL SHARE", + thumbAssetId: isAlbum ? dto.album.orElse(null)?.albumThumbnailAssetId : dto.assets.isNotEmpty ? dto.assets[0].id - : null; - - @override - String toString() => - 'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type, slug=$slug)'; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SharedLink && - other.id == id && - other.title == title && - other.thumbAssetId == thumbAssetId && - other.allowDownload == allowDownload && - other.allowUpload == allowUpload && - other.description == description && - other.password == password && - other.expiresAt == expiresAt && - other.key == key && - other.showMetadata == showMetadata && - other.type == type && - other.slug == slug; - - @override - int get hashCode => - id.hashCode ^ - title.hashCode ^ - thumbAssetId.hashCode ^ - allowDownload.hashCode ^ - allowUpload.hashCode ^ - description.hashCode ^ - password.hashCode ^ - expiresAt.hashCode ^ - key.hashCode ^ - showMetadata.hashCode ^ - type.hashCode ^ - slug.hashCode; + : null, + ); + } } diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 9e78fb4795..e2472f1063 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -43,7 +43,7 @@ class _DriftBackupPageState extends ConsumerState { void initState() { super.initState(); - WakelockPlus.enable(); + unawaited(WakelockPlus.enable()); final currentUser = ref.read(currentUserProvider); if (currentUser == null) { @@ -67,9 +67,9 @@ class _DriftBackupPageState extends ConsumerState { } @override - dispose() { + void dispose() { super.dispose(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); } @override @@ -81,8 +81,8 @@ class _DriftBackupPageState extends ConsumerState { final error = ref.watch(driftBackupProvider.select((p) => p.error)); - final backupNotifier = ref.read(driftBackupProvider.notifier); - final backupSyncManager = ref.read(backgroundSyncProvider); + final backupNotifier = ref.watch(driftBackupProvider.notifier); + final backupSyncManager = ref.watch(backgroundSyncProvider); Future startBackup() async { final currentUser = Store.tryGet(StoreKey.currentUser); @@ -111,7 +111,7 @@ class _DriftBackupPageState extends ConsumerState { title: Text("backup_controller_page_backup".t()), leading: IconButton( onPressed: () { - context.maybePop(true); + unawaited(context.maybePop(true)); }, splashRadius: 24, icon: const Icon(Icons.arrow_back_ios_rounded), @@ -119,7 +119,7 @@ class _DriftBackupPageState extends ConsumerState { actions: [ IconButton( onPressed: () { - context.pushRoute(const DriftBackupOptionsRoute()); + unawaited(context.pushRoute(const DriftBackupOptionsRoute())); }, icon: const Icon(Icons.settings_outlined), tooltip: "backup_options".t(context: context), @@ -143,7 +143,7 @@ class _DriftBackupPageState extends ConsumerState { onStart: () async => await startBackup(), onStop: () { syncSuccess = null; - backupNotifier.stopForegroundBackup(); + backupNotifier.stopForegroundBackup(reason: "backup button toggled off"); }, ), switch (error) { @@ -207,8 +207,8 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin } } - void showPermissionsDialog() { - showDialog( + Future showPermissionsDialog() { + return showDialog( context: context, builder: (ctx) => AlertDialog( content: Text(context.t.notification_permission_dialog_content), @@ -225,7 +225,7 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin expanded: false, onPressed: () { ContextHelper(context).pop(); - openAppSettings(); + unawaited(openAppSettings()); }, ), ], @@ -233,8 +233,8 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin ); } - void showBatteryOptimizationInfo() { - showDialog( + Future showBatteryOptimizationInfo() { + return showDialog( context: context, barrierDismissible: false, builder: (BuildContext ctx) { @@ -246,7 +246,8 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin labelText: context.t.backup_controller_page_background_battery_info_link, variant: .ghost, expanded: false, - onPressed: () => launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), + onPressed: () => + unawaited(launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication)), ), ImmichTextButton( labelText: context.t.backup_controller_page_background_battery_info_ok, @@ -279,11 +280,13 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), onPressed: () { - ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((p) { - if (p == PermissionStatus.permanentlyDenied) { - showPermissionsDialog(); - } - }); + unawaited( + ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((p) { + if (p == PermissionStatus.permanentlyDenied) { + unawaited(showPermissionsDialog()); + } + }), + ); }, ), if (notificationStatus != PermissionStatus.granted && batteryOptimizationStatus != PermissionStatus.granted) @@ -297,7 +300,7 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin textAlign: TextAlign.left, style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), - onPressed: showBatteryOptimizationInfo, + onPressed: () => unawaited(showBatteryOptimizationInfo()), ), ], TextButton.icon( @@ -318,7 +321,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { Widget buildSelectedAlbumName() { String text = "backup_controller_page_backup_selected".tr(); final albums = ref - .watch(backupAlbumProvider) + .read(backupAlbumProvider) .where((album) => album.backupSelection == BackupSelection.selected) .toList(); @@ -352,7 +355,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { Widget buildExcludedAlbumName() { String text = "backup_controller_page_excluded".tr(); final albums = ref - .watch(backupAlbumProvider) + .read(backupAlbumProvider) .where((album) => album.backupSelection == BackupSelection.excluded) .toList(); @@ -400,6 +403,10 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { trailing: ElevatedButton( onPressed: () async { await context.pushRoute(const DriftBackupAlbumSelectionRoute()); + if (!context.mounted) { + return; + } + final currentUser = ref.read(currentUserProvider); if (currentUser == null) { return; @@ -559,6 +566,11 @@ class _PreparingStatusState extends ConsumerState { final currentUser = ref.read(currentUserProvider); if (currentUser != null && mounted) { await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + if (!context.mounted) { + timer.cancel(); + _pollingTimer = null; + return; + } // Stop polling if processing count reaches 0 final updatedProcessingCount = ref.read(driftBackupProvider.select((p) => p.processingCount)); 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 b0616589f1..de0ad52b5a 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -12,8 +12,8 @@ import 'package:immich_mobile/infrastructure/repositories/settings.repository.da import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -102,6 +102,9 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; final backupNotifier = ref.read(driftBackupProvider.notifier); @@ -119,7 +126,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState backgroundSync.hashAssets())); if (isBackupEnabled) { - backupNotifier.stopForegroundBackup(); + backupNotifier.stopForegroundBackup(reason: "backup albums updated"); unawaited( backgroundSync.syncRemote().then((success) { if (success) { @@ -132,6 +139,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState removeSelection() { + return ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } return Padding( padding: const EdgeInsets.only(right: 8.0), child: GestureDetector( - onTap: removeSelection, + onTap: () => unawaited(removeSelection()), child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, @@ -387,7 +400,7 @@ class _SelectedAlbumNameChips extends ConsumerWidget { backgroundColor: context.primaryColor, deleteIconColor: context.isDarkTheme ? Colors.black : Colors.white, deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, + onDeleted: () => unawaited(removeSelection()), ), ), ), @@ -408,12 +421,12 @@ class _ExcludedAlbumNameChips extends ConsumerWidget { children: excludedBackupAlbums.asMap().entries.map((entry) { final album = entry.value; - void removeSelection() { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + Future removeSelection() { + return ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } return GestureDetector( - onTap: removeSelection, + onTap: () => unawaited(removeSelection()), child: Padding( padding: const EdgeInsets.only(right: 8.0), child: AnimatedContainer( @@ -427,7 +440,7 @@ class _ExcludedAlbumNameChips extends ConsumerWidget { backgroundColor: Colors.red[300], deleteIconColor: context.scaffoldBackgroundColor, deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, + onDeleted: () => unawaited(removeSelection()), ), ), ), @@ -457,7 +470,7 @@ class _SelectAllButton extends ConsumerWidget { ? () { for (final album in filteredAlbums) { if (album.backupSelection != BackupSelection.selected) { - ref.read(backupAlbumProvider.notifier).selectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).selectAlbum(album)); } } } @@ -477,7 +490,7 @@ class _SelectAllButton extends ConsumerWidget { ? () { for (final album in filteredAlbums) { if (album.backupSelection == BackupSelection.selected) { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).deselectAlbum(album)); } } } diff --git a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart index 36d51c5624..4f66d6d6fb 100644 --- a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart +++ b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart @@ -20,7 +20,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> result = ref.watch(driftBackupCandidateProvider); + final AsyncValue> result = ref.watch(driftBackupCandidateProvider); return Scaffold( appBar: AppBar(title: Text('backup_controller_page_remainder'.t(context: context))), body: result.when( @@ -88,6 +88,10 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { ), onTap: () async { await context.maybePop(); + if (!context.mounted) { + return; + } + await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()])); EventStream.shared.emit(ScrollToDateEvent(asset.createdAt)); }, diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index 42643a0496..ccd486e04d 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -19,7 +19,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { bool hasPopped = false; - final previousBackup = ref.read(appConfigProvider).backup; + final previousBackup = ref.watch(appConfigProvider.select((s) => s.backup)); final previousCellularForVideos = previousBackup.useCellularForVideos; final previousCellularForPhotos = previousBackup.useCellularForPhotos; return PopScope( @@ -46,7 +46,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled; - if (!isBackupEnabled) { + if (!isBackupEnabled || !context.mounted) { return; } @@ -59,7 +59,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { final backupNotifier = ref.read(driftBackupProvider.notifier); final backgroundSync = ref.read(backgroundSyncProvider); - backupNotifier.stopForegroundBackup(); + backupNotifier.stopForegroundBackup(reason: "backup settings updated"); unawaited( backgroundSync.syncRemote().then((success) { if (success) { diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index 978fcf7c57..5ed43672d9 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -429,7 +429,7 @@ class _DriftUploadDetailPageState extends ConsumerState { SizedBox( width: 48, height: 48, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.outline.withValues(alpha: 0.1), borderRadius: const BorderRadius.all(Radius.circular(8)), @@ -520,7 +520,7 @@ class _CurrentUploadThumbnail extends ConsumerWidget { ), clipBehavior: Clip.antiAlias, child: snapshot.data != null - ? Thumbnail.fromAsset(asset: snapshot.data!, size: const Size(48, 48), fit: BoxFit.cover) + ? Thumbnail.fromAsset(asset: snapshot.data, size: const Size(48, 48), fit: BoxFit.cover) : Icon(Icons.image, size: 24, color: context.colorScheme.primary), ), ); diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 336bf0b605..370d6a106d 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -1,8 +1,9 @@ +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/domain/models/log.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -11,11 +12,11 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; @RoutePage() -class AppLogPage extends HookConsumerWidget { +class AppLogPage extends HookWidget { const AppLogPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final immichLogger = LogService.I; final shouldReload = useState(false); final logMessages = useFuture(useMemoized(() => immichLogger.getMessages(), [shouldReload.value])); @@ -61,7 +62,7 @@ class AppLogPage extends HookConsumerWidget { size: 20.0, ), onPressed: () { - immichLogger.clearLogs(); + unawaited(immichLogger.clearLogs()); shouldReload.value = !shouldReload.value; }, ), @@ -70,7 +71,7 @@ class AppLogPage extends HookConsumerWidget { return IconButton( icon: Icon(Icons.share_rounded, color: context.primaryColor, semanticLabel: "Share logs", size: 20.0), onPressed: () { - ImmichLogger.shareLogs(iconContext); + unawaited(ImmichLogger.shareLogs(iconContext)); }, ); }, @@ -78,7 +79,7 @@ class AppLogPage extends HookConsumerWidget { ], leading: IconButton( onPressed: () { - context.maybePop(); + unawaited(context.maybePop()); }, icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20.0), ), @@ -90,7 +91,7 @@ class AppLogPage extends HookConsumerWidget { }, itemCount: logMessages.data?.length ?? 0, itemBuilder: (context, index) { - var logMessage = logMessages.data![index]; + final logMessage = logMessages.data![index]; return ListTile( onTap: () => context.pushRoute(AppLogDetailRoute(logMessage: logMessage)), trailing: const Icon(Icons.arrow_forward_ios_rounded), @@ -116,7 +117,7 @@ class AppLogPage extends HookConsumerWidget { /// Truncate the log message to a certain number of lines /// @param int maxLines - Max number of lines to truncate String truncateLogMessage(String message, int maxLines) { - List messageLines = message.split("\n"); + final List messageLines = message.split("\n"); if (messageLines.length < maxLines) { return message; } diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 890e46888f..bc3d26a019 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -1,20 +1,22 @@ +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:hooks_riverpod/hooks_riverpod.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'; @RoutePage() -class AppLogDetailPage extends HookConsumerWidget { +class AppLogDetailPage extends HookWidget { const AppLogDetailPage({super.key, required this.logMessage}); final LogMessage logMessage; @override - Widget build(BuildContext context, WidgetRef ref) { - buildTextWithCopyButton(String header, String text) { + Widget build(BuildContext context) { + Padding buildTextWithCopyButton(String header, String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -33,22 +35,28 @@ class AppLogDetailPage extends HookConsumerWidget { ), IconButton( onPressed: () { - Clipboard.setData(ClipboardData(text: text)).then((_) { - context.scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "copied_to_clipboard".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + unawaited( + Clipboard.setData(ClipboardData(text: text)).then((_) { + if (!context.mounted) { + return; + } + + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + "copied_to_clipboard".tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + ), ), - ), - ); - }); + ); + }), + ); }, icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor), ), ], ), - Container( + DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all(Radius.circular(15.0)), @@ -66,7 +74,7 @@ class AppLogDetailPage extends HookConsumerWidget { ); } - buildLogContext(String logger) { + Padding buildLogContext(String logger) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -79,7 +87,7 @@ class AppLogDetailPage extends HookConsumerWidget { style: TextStyle(fontSize: 12.0, color: context.primaryColor, fontWeight: FontWeight.bold), ), ), - Container( + DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all(Radius.circular(15.0)), @@ -87,7 +95,7 @@ class AppLogDetailPage extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.all(8.0), child: SelectableText( - logger.toString(), + logger, style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"), ), ), diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart index 0775f5b4e4..2015948fad 100644 --- a/mobile/lib/pages/common/download_panel.dart +++ b/mobile/lib/pages/common/download_panel.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:background_downloader/background_downloader.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -14,8 +16,8 @@ class DownloadPanel extends ConsumerWidget { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); - onCancelDownload(String id) { - ref.watch(downloadStateProvider.notifier).cancelDownload(id); + void onCancelDownload(String id) { + unawaited(ref.read(downloadStateProvider.notifier).cancelDownload(id)); } return Positioned( diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index 9a6b602b04..e0228eecac 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -21,7 +21,7 @@ class HeaderSettingsPage extends HookConsumerWidget { final headers = useState>([]); final setInitialHeaders = useState(false); - final storedHeaders = ref.read(appConfigProvider).network.customHeaders; + final storedHeaders = ref.watch(appConfigProvider.select((s) => s.network.customHeaders)); if (!setInitialHeaders.value) { storedHeaders.forEach((k, v) { final header = SettingsHeader(); @@ -41,7 +41,7 @@ class HeaderSettingsPage extends HookConsumerWidget { } setInitialHeaders.value = true; - var list = [ + final list = [ ...headers.value.map((headerValue) { return HeaderKeyValueSettings( header: headerValue, @@ -81,7 +81,7 @@ class HeaderSettingsPage extends HookConsumerWidget { ); } - saveHeaders(WidgetRef ref, List headers) async { + Future saveHeaders(WidgetRef ref, List headers) async { final headersMap = {}; for (final header in headers) { final key = header.key.trim(); @@ -93,8 +93,9 @@ class HeaderSettingsPage extends HookConsumerWidget { headersMap[key] = value; } + final apiService = ref.read(apiServiceProvider); await ref.read(settingsProvider).write(.networkCustomHeaders, headersMap); - await ref.read(apiServiceProvider).updateHeaders(); + await apiService.updateHeaders(); } } diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart index 4563834473..58528008ca 100644 --- a/mobile/lib/pages/common/large_leading_tile.dart +++ b/mobile/lib/pages/common/large_leading_tile.dart @@ -34,7 +34,7 @@ class LargeLeadingTile extends StatelessWidget { return InkWell( borderRadius: BorderRadius.circular(borderRadius), onTap: disabled ? null : onTap, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: selected ? selectedTileColor ?? Theme.of(context).primaryColor.withAlpha(30) diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 0d423875cb..65a78aed8a 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -25,7 +25,7 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; -import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; +import 'package:url_launcher/url_launcher.dart' show LaunchMode, launchUrl; class BootstrapErrorWidget extends StatelessWidget { final String error; @@ -135,9 +135,11 @@ class _BottomPanelState extends State<_BottomPanel> { return; } - if (mounted) { - setState(() => _cleared = true); + if (!mounted) { + return; } + + setState(() => _cleared = true); } @override @@ -282,11 +284,13 @@ class SplashScreenPageState extends ConsumerState { @override void initState() { super.initState(); - ref - .read(authProvider.notifier) - .setOpenApiServiceEndpoint() - .then(logConnectionInfo) - .whenComplete(() => resumeSession()); + unawaited( + ref + .read(authProvider.notifier) + .setOpenApiServiceEndpoint() + .then(logConnectionInfo) + .whenComplete(() => resumeSession()), + ); } void logConnectionInfo(String? endpoint) { @@ -297,7 +301,7 @@ class SplashScreenPageState extends ConsumerState { log.info("Resuming session at $endpoint"); } - void resumeSession() async { + Future resumeSession() async { final serverUrl = Store.tryGet(StoreKey.serverUrl); final endpoint = Store.tryGet(StoreKey.serverEndpoint); final accessToken = Store.tryGet(StoreKey.accessToken); @@ -310,46 +314,53 @@ class SplashScreenPageState extends ConsumerState { final viewIntentHandler = ref.read(viewIntentHandlerProvider); unawaited( - ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( - (_) async { - try { - wsProvider.connect(); - unawaited(infoProvider.getServerInfo()); + ref + .read(authProvider.notifier) + .saveAuthInfo(accessToken: accessToken) + .then( + (_) async { + try { + wsProvider.connect(); + unawaited(infoProvider.getServerInfo()); - bool syncSuccess = false; - await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), - ]); + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); - await viewIntentHandler.flushDeferredViewIntent(); + await viewIntentHandler.flushDeferredViewIntent(); - if (syncSuccess) { - await Future.wait([ - backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); - }), - _resumeBackup(backupProvider), - // TODO: Bring back when the soft freeze issue is addressed - // backgroundManager.syncCloudIds(), - ]); - } else { - await backgroundManager.hashAssets(); - } + if (syncSuccess) { + await Future.wait([ + backgroundManager.hashAssets().then((_) { + unawaited(_resumeBackup(backupProvider)); + }), + _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), + ]); + } else { + await backgroundManager.hashAssets(); + } - if (SettingsRepository.instance.appConfig.backup.syncAlbums) { - await backgroundManager.syncLinkedAlbum(); - } - } catch (e) { - log.severe('Failed establishing connection to the server: $e'); - } - }, - onError: (exception) => { - log.severe('Failed to update auth info with access token: $accessToken'), - ref.read(authProvider.notifier).logout(), - context.router.replaceAll([const LoginRoute()]), - }, - ), + if (SettingsRepository.instance.appConfig.backup.syncAlbums) { + await backgroundManager.syncLinkedAlbum(); + } + } catch (e) { + log.severe('Failed establishing connection to the server: $e'); + } + }, + onError: (exception) { + log.severe('Failed to update auth info with access token: $accessToken'); + if (!mounted) { + return; + } + + unawaited(ref.read(authProvider.notifier).logout()); + unawaited(context.router.replaceAll([const LoginRoute()])); + }, + ), ); } else { log.severe('Missing crucial offline login info - Logging out completely'); diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index 2fdcec4054..f834e4ac51 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -126,7 +126,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { // Album page if (index == kAlbumTabIndex) { - ref.read(remoteAlbumProvider.notifier).refresh(); + unawaited(ref.read(remoteAlbumProvider.notifier).refresh()); } // Library page @@ -168,7 +168,7 @@ class _BottomNavigationBarState extends ConsumerState<_BottomNavigationBar> { @override void dispose() { - _eventSubscription?.cancel(); + unawaited(_eventSubscription?.cancel()); super.dispose(); } diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 5efb5ccc62..69631efe4e 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -53,7 +55,7 @@ class FolderPage extends HookConsumerWidget { useEffect(() { if (folder == null) { - ref.read(folderStructureProvider.notifier).fetchFolders(sortOrder.value); + unawaited(ref.read(folderStructureProvider.notifier).fetchFolders(sortOrder.value)); } return null; }, []); @@ -72,7 +74,7 @@ class FolderPage extends HookConsumerWidget { void onToggleSortOrder() { final newOrder = sortOrder.value == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - ref.read(folderStructureProvider.notifier).fetchFolders(newOrder); + unawaited(ref.read(folderStructureProvider.notifier).fetchFolders(newOrder)); sortOrder.value = newOrder; } @@ -89,7 +91,7 @@ class FolderPage extends HookConsumerWidget { if (folder == null) { return FolderContent(folder: rootFolder, root: rootFolder, sortOrder: sortOrder.value); } else { - return FolderContent(folder: currentFolder.value!, root: rootFolder, sortOrder: sortOrder.value); + return FolderContent(folder: currentFolder.value, root: rootFolder, sortOrder: sortOrder.value); } }, loading: () => const Center(child: CircularProgressIndicator()), @@ -118,7 +120,7 @@ class FolderContent extends HookConsumerWidget { if (folder == null) { return; } - ref.read(folderRenderListProvider(folder!).notifier).fetchAssets(sortOrder); + unawaited(ref.read(folderRenderListProvider(folder!).notifier).fetchAssets(sortOrder)); return null; }, [folder]); @@ -126,7 +128,7 @@ class FolderContent extends HookConsumerWidget { return Center(child: const Text("folder_not_found").tr()); } - getSubtitle(int subFolderCount) { + String getSubtitle(int subFolderCount) { if (subFolderCount > 0) { return "$subFolderCount ${tr("folders")}".toLowerCase(); } @@ -176,12 +178,14 @@ class FolderContent extends HookConsumerWidget { (index, asset) => LargeLeadingTile( onTap: () { AssetViewer.setAsset(ref, asset); - context.pushRoute( - AssetViewerRoute( - initialIndex: index, - timelineService: ref - .read(timelineFactoryProvider) - .fromAssets(folderAssets, TimelineOrigin.folder), + unawaited( + context.pushRoute( + AssetViewerRoute( + initialIndex: index, + timelineService: ref + .read(timelineFactoryProvider) + .fromAssets(folderAssets, TimelineOrigin.folder), + ), ), ); }, diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index 3af320dc5f..81e08d853b 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -25,21 +25,23 @@ class PinAuthPage extends HookConsumerWidget { Future registerBiometric(String pinCode) async { final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric(context, pinCode); - if (isRegistered) { - context.showSnackBar( - SnackBar( - content: Text('biometric_auth_enabled'.tr(), style: context.textTheme.labelLarge), - duration: const Duration(seconds: 3), - backgroundColor: context.colorScheme.primaryContainer, - ), - ); - - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); + if (!isRegistered || !context.mounted) { + return; } + + context.showSnackBar( + SnackBar( + content: Text('biometric_auth_enabled'.tr(), style: context.textTheme.labelLarge), + duration: const Duration(seconds: 3), + backgroundColor: context.colorScheme.primaryContainer, + ), + ); + + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); } - enableBiometricAuth() { - showDialog( + Future enableBiometricAuth() { + return showDialog( context: context, builder: (buildContext) { return SimpleDialog( @@ -53,7 +55,7 @@ class PinAuthPage extends HookConsumerWidget { description: 'enable_biometric_auth_description'.tr(), onSuccess: (pinCode) { Navigator.pop(buildContext); - registerBiometric(pinCode); + unawaited(registerBiometric(pinCode)); }, autoFocus: true, icon: Icons.fingerprint_rounded, @@ -83,7 +85,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - context.replaceRoute(const DriftLockedFolderRoute()); + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); }, ), ), @@ -93,7 +95,7 @@ class PinAuthPage extends HookConsumerWidget { padding: const EdgeInsets.only(right: 16.0), child: TextButton.icon( icon: const Icon(Icons.fingerprint, size: 28), - onPressed: enableBiometricAuth, + onPressed: () => unawaited(enableBiometricAuth()), label: Text( 'use_biometric'.tr(), style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor, fontSize: 18), diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart index 7274b8a14e..afe64d24f4 100644 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ b/mobile/lib/pages/library/partner/partner.page.dart @@ -33,7 +33,7 @@ class PartnerPage extends ConsumerWidget { title: Text(context.t.partners), elevation: 0, centerTitle: false, - actions: const [ActionIconButtonWidget(action: PartnerAddAction())], + actions: const [ActionIconButton(action: PartnerAddAction())], ), body: sharedByAsync.when( data: (partners) => PartnerSharedByList(partners: partners.toList(growable: false)), @@ -60,7 +60,7 @@ class _EmptyPartners extends StatelessWidget { ), const Align( alignment: .center, - child: ActionButtonWidget(action: PartnerAddAction()), + child: ActionButton(action: PartnerAddAction()), ), ], ), @@ -88,7 +88,7 @@ class PartnerSharedByList extends StatelessWidget { leading: PartnerUserAvatar(userId: partner.id, name: partner.name), title: Text(partner.name), subtitle: Text(partner.email), - trailing: ActionIconButtonWidget( + trailing: ActionIconButton( action: PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name), ), ); 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 a4f52ebd4c..18fc235dd8 100644 --- a/mobile/lib/pages/library/shared_link/shared_link.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -17,7 +19,7 @@ class SharedLinkPage extends HookConsumerWidget { final sharedLinks = ref.watch(sharedLinksStateProvider); useEffect(() { - ref.read(sharedLinksStateProvider.notifier).fetchLinks(); + unawaited(ref.read(sharedLinksStateProvider.notifier).fetchLinks()); return () { if (!context.mounted) { return; diff --git a/mobile/lib/pages/login/change_password.page.dart b/mobile/lib/pages/login/change_password.page.dart index 248526df1b..051f7f408c 100644 --- a/mobile/lib/pages/login/change_password.page.dart +++ b/mobile/lib/pages/login/change_password.page.dart @@ -1,14 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/widgets/forms/change_password_form.dart'; @RoutePage() -class ChangePasswordPage extends HookConsumerWidget { +class ChangePasswordPage extends StatelessWidget { const ChangePasswordPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - return const Scaffold(body: ChangePasswordForm()); - } + Widget build(BuildContext context) => const Scaffold(body: ChangePasswordForm()); } diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index 5f40b32baa..228fa1cc30 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -1,28 +1,29 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/forms/login/login_form.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/forms/login/login_form.dart'; import 'package:package_info_plus/package_info_plus.dart'; @RoutePage() -class LoginPage extends HookConsumerWidget { +class LoginPage extends HookWidget { const LoginPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final appVersion = useState('0.0.0'); - getAppInfo() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); + Future getAppInfo() async { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); appVersion.value = packageInfo.version; } useEffect(() { - getAppInfo(); + unawaited(getAppInfo()); return null; }); @@ -55,7 +56,7 @@ class LoginPage extends HookConsumerWidget { ), ), onTap: () { - context.pushRoute(const AppLogRoute()); + unawaited(context.pushRoute(const AppLogRoute())); }, ), ], 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 3dace15ced..e8a507bfdc 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -1,10 +1,10 @@ +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:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; @@ -13,13 +13,13 @@ import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() -class MapLocationPickerPage extends HookConsumerWidget { +class MapLocationPickerPage extends HookWidget { final LatLng initialLatLng; const MapLocationPickerPage({super.key, this.initialLatLng = const LatLng(0, 0)}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final selectedLatLng = useValueNotifier(initialLatLng); final controller = useRef(null); final marker = useRef(null); @@ -37,17 +37,17 @@ class MapLocationPickerPage extends HookConsumerWidget { } void onClose([LatLng? selected]) { - context.maybePop(selected); + unawaited(context.maybePop(selected)); } Future getCurrentLocation() async { - var (currentLocation, _) = await MapUtils.checkPermAndGetLocation(context: context); + final (currentLocation, _) = await MapUtils.checkPermAndGetLocation(context: context); if (currentLocation == null) { return; } - var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); + final currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); selectedLatLng.value = currentLatLng; await controller.value?.animateCamera(CameraUpdate.newLatLngZoom(currentLatLng, 12)); } diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 2744b187de..db5e583d7a 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -35,7 +37,7 @@ class ShareIntentPage extends ConsumerWidget { ref.read(shareIntentUploadProvider.notifier).addAttachments(attachments); } - void upload() async { + Future upload() async { final files = candidates.map((candidate) => candidate.file).toList(); await ref.read(shareIntentUploadProvider.notifier).uploadAll(files); } @@ -65,7 +67,7 @@ class ShareIntentPage extends ConsumerWidget { ), leading: IconButton( onPressed: () { - context.navigateTo(const TabShellRoute()); + unawaited(context.navigateTo(const TabShellRoute())); }, icon: const Icon(Icons.arrow_back), ), @@ -102,7 +104,7 @@ class ShareIntentPage extends ConsumerWidget { Icons.image, color: Colors.white, size: 20, - shadows: [Shadow(offset: Offset(0, 0), blurRadius: 8.0, color: Colors.black45)], + shadows: [Shadow(offset: Offset.zero, blurRadius: 8.0, color: Colors.black45)], ), ), ], diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart deleted file mode 100644 index 34f4c41b48..0000000000 --- a/mobile/lib/platform/background_worker_api.g.dart +++ /dev/null @@ -1,365 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { - if (empty) { - return []; - } - if (error == null) { - return [result]; - } - return [error.code, error.message, error.details]; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -class BackgroundWorkerSettings { - BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds}); - - bool requiresCharging; - - int minimumDelaySeconds; - - List _toList() { - return [requiresCharging, minimumDelaySeconds]; - } - - Object encode() { - return _toList(); - } - - static BackgroundWorkerSettings decode(Object result) { - result as List; - return BackgroundWorkerSettings(requiresCharging: result[0]! as bool, minimumDelaySeconds: result[1]! as int); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! BackgroundWorkerSettings || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(requiresCharging, other.requiresCharging) && - _deepEquals(minimumDelaySeconds, other.minimumDelaySeconds); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is BackgroundWorkerSettings) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - return BackgroundWorkerSettings.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class BackgroundWorkerFgHostApi { - /// Constructor for [BackgroundWorkerFgHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - BackgroundWorkerFgHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future enable() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future saveNotificationMessage(String title, String body) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, body]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future configure(BackgroundWorkerSettings settings) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([settings]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future disable() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } -} - -class BackgroundWorkerBgHostApi { - /// Constructor for [BackgroundWorkerBgHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - BackgroundWorkerBgHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future onInitialized() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future close() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } -} - -abstract class BackgroundWorkerFlutterApi { - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - Future onIosUpload(bool isRefresh, int? maxSeconds); - - Future onAndroidUpload(int? maxMinutes); - - Future cancel(); - - static void setUp( - BackgroundWorkerFlutterApi? api, { - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - { - final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - final List args = message! as List; - final bool arg_isRefresh = args[0]! as bool; - final int? arg_maxSeconds = args[1] as int?; - try { - await api.onIosUpload(arg_isRefresh, arg_maxSeconds); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString()), - ); - } - }); - } - } - { - final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - final List args = message! as List; - final int? arg_maxMinutes = args[0] as int?; - try { - await api.onAndroidUpload(arg_maxMinutes); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString()), - ); - } - }); - } - } - { - final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - try { - await api.cancel(); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString()), - ); - } - }); - } - } - } -} diff --git a/mobile/lib/platform/background_worker_lock_api.g.dart b/mobile/lib/platform/background_worker_lock_api.g.dart deleted file mode 100644 index c7836c4c69..0000000000 --- a/mobile/lib/platform/background_worker_lock_api.g.dart +++ /dev/null @@ -1,90 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class BackgroundWorkerLockApi { - /// Constructor for [BackgroundWorkerLockApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - BackgroundWorkerLockApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future lock() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future unlock() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } -} diff --git a/mobile/lib/platform/connectivity_api.g.dart b/mobile/lib/platform/connectivity_api.g.dart deleted file mode 100644 index 8cf8979532..0000000000 --- a/mobile/lib/platform/connectivity_api.g.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -enum NetworkCapability { cellular, wifi, vpn, unmetered } - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is NetworkCapability) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final value = readValue(buffer) as int?; - return value == null ? null : NetworkCapability.values[value]; - default: - return super.readValueOfType(type, buffer); - } - } -} - -class ConnectivityApi { - /// Constructor for [ConnectivityApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - ConnectivityApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future> getCapabilities() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } -} diff --git a/mobile/lib/platform/local_image_api.g.dart b/mobile/lib/platform/local_image_api.g.dart deleted file mode 100644 index fbd0876735..0000000000 --- a/mobile/lib/platform/local_image_api.g.dart +++ /dev/null @@ -1,128 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class LocalImageApi { - /// Constructor for [LocalImageApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - LocalImageApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future?> requestImage( - String assetId, { - required int requestId, - required int width, - required int height, - required bool isVideo, - required bool preferEncoded, - }) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - assetId, - requestId, - width, - height, - isVideo, - preferEncoded, - ]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ); - return (pigeonVar_replyValue as Map?)?.cast(); - } - - Future cancelRequest(int requestId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future> getThumbhash(String thumbhash) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([thumbhash]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as Map).cast(); - } -} diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart deleted file mode 100644 index bd979af87b..0000000000 --- a/mobile/lib/platform/native_sync_api.g.dart +++ /dev/null @@ -1,708 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping } - -class PlatformAsset { - PlatformAsset({ - required this.id, - required this.name, - required this.type, - this.createdAt, - this.updatedAt, - this.width, - this.height, - required this.durationMs, - required this.orientation, - required this.isFavorite, - this.adjustmentTime, - this.latitude, - this.longitude, - required this.playbackStyle, - }); - - String id; - - String name; - - int type; - - int? createdAt; - - int? updatedAt; - - int? width; - - int? height; - - int durationMs; - - int orientation; - - bool isFavorite; - - int? adjustmentTime; - - double? latitude; - - double? longitude; - - PlatformAssetPlaybackStyle playbackStyle; - - List _toList() { - return [ - id, - name, - type, - createdAt, - updatedAt, - width, - height, - durationMs, - orientation, - isFavorite, - adjustmentTime, - latitude, - longitude, - playbackStyle, - ]; - } - - Object encode() { - return _toList(); - } - - static PlatformAsset decode(Object result) { - result as List; - return PlatformAsset( - id: result[0]! as String, - name: result[1]! as String, - type: result[2]! as int, - createdAt: result[3] as int?, - updatedAt: result[4] as int?, - width: result[5] as int?, - height: result[6] as int?, - durationMs: result[7]! as int, - orientation: result[8]! as int, - isFavorite: result[9]! as bool, - adjustmentTime: result[10] as int?, - latitude: result[11] as double?, - longitude: result[12] as double?, - playbackStyle: result[13]! as PlatformAssetPlaybackStyle, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! PlatformAsset || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(id, other.id) && - _deepEquals(name, other.name) && - _deepEquals(type, other.type) && - _deepEquals(createdAt, other.createdAt) && - _deepEquals(updatedAt, other.updatedAt) && - _deepEquals(width, other.width) && - _deepEquals(height, other.height) && - _deepEquals(durationMs, other.durationMs) && - _deepEquals(orientation, other.orientation) && - _deepEquals(isFavorite, other.isFavorite) && - _deepEquals(adjustmentTime, other.adjustmentTime) && - _deepEquals(latitude, other.latitude) && - _deepEquals(longitude, other.longitude) && - _deepEquals(playbackStyle, other.playbackStyle); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class PlatformAlbum { - PlatformAlbum({ - required this.id, - required this.name, - this.updatedAt, - required this.isCloud, - required this.assetCount, - }); - - String id; - - String name; - - int? updatedAt; - - bool isCloud; - - int assetCount; - - List _toList() { - return [id, name, updatedAt, isCloud, assetCount]; - } - - Object encode() { - return _toList(); - } - - static PlatformAlbum decode(Object result) { - result as List; - return PlatformAlbum( - id: result[0]! as String, - name: result[1]! as String, - updatedAt: result[2] as int?, - isCloud: result[3]! as bool, - assetCount: result[4]! as int, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! PlatformAlbum || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(id, other.id) && - _deepEquals(name, other.name) && - _deepEquals(updatedAt, other.updatedAt) && - _deepEquals(isCloud, other.isCloud) && - _deepEquals(assetCount, other.assetCount); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class SyncDelta { - SyncDelta({required this.hasChanges, required this.updates, required this.deletes, required this.assetAlbums}); - - bool hasChanges; - - List updates; - - List deletes; - - Map> assetAlbums; - - List _toList() { - return [hasChanges, updates, deletes, assetAlbums]; - } - - Object encode() { - return _toList(); - } - - static SyncDelta decode(Object result) { - result as List; - return SyncDelta( - hasChanges: result[0]! as bool, - updates: (result[1]! as List).cast(), - deletes: (result[2]! as List).cast(), - assetAlbums: (result[3]! as Map).cast>(), - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! SyncDelta || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(hasChanges, other.hasChanges) && - _deepEquals(updates, other.updates) && - _deepEquals(deletes, other.deletes) && - _deepEquals(assetAlbums, other.assetAlbums); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class HashResult { - HashResult({required this.assetId, this.error, this.hash}); - - String assetId; - - String? error; - - String? hash; - - List _toList() { - return [assetId, error, hash]; - } - - Object encode() { - return _toList(); - } - - static HashResult decode(Object result) { - result as List; - return HashResult(assetId: result[0]! as String, error: result[1] as String?, hash: result[2] as String?); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! HashResult || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(assetId, other.assetId) && _deepEquals(error, other.error) && _deepEquals(hash, other.hash); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class CloudIdResult { - CloudIdResult({required this.assetId, this.error, this.cloudId}); - - String assetId; - - String? error; - - String? cloudId; - - List _toList() { - return [assetId, error, cloudId]; - } - - Object encode() { - return _toList(); - } - - static CloudIdResult decode(Object result) { - result as List; - return CloudIdResult(assetId: result[0]! as String, error: result[1] as String?, cloudId: result[2] as String?); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! CloudIdResult || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(assetId, other.assetId) && - _deepEquals(error, other.error) && - _deepEquals(cloudId, other.cloudId); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is PlatformAssetPlaybackStyle) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else if (value is PlatformAsset) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else if (value is PlatformAlbum) { - buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else if (value is SyncDelta) { - buffer.putUint8(132); - writeValue(buffer, value.encode()); - } else if (value is HashResult) { - buffer.putUint8(133); - writeValue(buffer, value.encode()); - } else if (value is CloudIdResult) { - buffer.putUint8(134); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final value = readValue(buffer) as int?; - return value == null ? null : PlatformAssetPlaybackStyle.values[value]; - case 130: - return PlatformAsset.decode(readValue(buffer)!); - case 131: - return PlatformAlbum.decode(readValue(buffer)!); - case 132: - return SyncDelta.decode(readValue(buffer)!); - case 133: - return HashResult.decode(readValue(buffer)!); - case 134: - return CloudIdResult.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class NativeSyncApi { - /// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - NativeSyncApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future shouldFullSync() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future getMediaChanges() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as SyncDelta; - } - - Future checkpointSync() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future clearSyncCheckpoint() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future> getAssetIdsForAlbum(String albumId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future> getAlbums() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future getAssetsCountSince(String albumId, int timestamp) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId, timestamp]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as int; - } - - Future> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId, updatedTimeCond]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future> hashAssets(List assetIds, {bool allowNetworkAccess = false}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds, allowNetworkAccess]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future cancelHashing() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future cancelSync() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future>> getTrashedAssets() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as Map).cast>(); - } - - Future restoreFromTrashById(String mediaId, int type) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([mediaId, type]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future> getCloudIdForAssetIds(List assetIds) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } -} diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart deleted file mode 100644 index 6258060bfb..0000000000 --- a/mobile/lib/platform/network_api.g.dart +++ /dev/null @@ -1,331 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -class ClientCertData { - ClientCertData({required this.data, required this.password}); - - Uint8List data; - - String password; - - List _toList() { - return [data, password]; - } - - Object encode() { - return _toList(); - } - - static ClientCertData decode(Object result) { - result as List; - return ClientCertData(data: result[0]! as Uint8List, password: result[1]! as String); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! ClientCertData || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(data, other.data) && _deepEquals(password, other.password); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class ClientCertPrompt { - ClientCertPrompt({required this.title, required this.message, required this.cancel, required this.confirm}); - - String title; - - String message; - - String cancel; - - String confirm; - - List _toList() { - return [title, message, cancel, confirm]; - } - - Object encode() { - return _toList(); - } - - static ClientCertPrompt decode(Object result) { - result as List; - return ClientCertPrompt( - title: result[0]! as String, - message: result[1]! as String, - cancel: result[2]! as String, - confirm: result[3]! as String, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! ClientCertPrompt || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(title, other.title) && - _deepEquals(message, other.message) && - _deepEquals(cancel, other.cancel) && - _deepEquals(confirm, other.confirm); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is ClientCertData) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else if (value is ClientCertPrompt) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - return ClientCertData.decode(readValue(buffer)!); - case 130: - return ClientCertPrompt.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class NetworkApi { - /// Constructor for [NetworkApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - NetworkApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future addCertificate(ClientCertData clientData) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([clientData]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future selectCertificate(ClientCertPrompt promptText) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([promptText]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future removeCertificate() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future hasCertificate() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future getClientPointer() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as int; - } - - Future setRequestHeaders(Map headers, List serverUrls, String? token) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([headers, serverUrls, token]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future getAppGroupId() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as String; - } -} diff --git a/mobile/lib/platform/permission_api.g.dart b/mobile/lib/platform/permission_api.g.dart deleted file mode 100644 index 7b85d611d2..0000000000 --- a/mobile/lib/platform/permission_api.g.dart +++ /dev/null @@ -1,146 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -enum PermissionStatus { granted, denied, permanentlyDenied } - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is PermissionStatus) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final value = readValue(buffer) as int?; - return value == null ? null : PermissionStatus.values[value]; - default: - return super.readValueOfType(type, buffer); - } - } -} - -class PermissionApi { - /// Constructor for [PermissionApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - PermissionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future isIgnoringBatteryOptimizations() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as PermissionStatus; - } - - Future hasManageMediaPermission() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future requestManageMediaPermission() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future manageMediaPermission() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } -} diff --git a/mobile/lib/platform/remote_image_api.g.dart b/mobile/lib/platform/remote_image_api.g.dart deleted file mode 100644 index 5239cb3e45..0000000000 --- a/mobile/lib/platform/remote_image_api.g.dart +++ /dev/null @@ -1,114 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class RemoteImageApi { - /// Constructor for [RemoteImageApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - RemoteImageApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future?> requestImage(String url, {required int requestId, required bool preferEncoded}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, requestId, preferEncoded]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ); - return (pigeonVar_replyValue as Map?)?.cast(); - } - - Future cancelRequest(int requestId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future clearCache() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as int; - } -} diff --git a/mobile/lib/platform/thumbnail_api.g.dart b/mobile/lib/platform/thumbnail_api.g.dart deleted file mode 100644 index 53d7b10fc3..0000000000 --- a/mobile/lib/platform/thumbnail_api.g.dart +++ /dev/null @@ -1,142 +0,0 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; - -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; -import 'package:flutter/services.dart'; - -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class ThumbnailApi { - /// Constructor for [ThumbnailApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - ThumbnailApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future> requestImage( - String assetId, { - required int requestId, - required int width, - required int height, - required bool isVideo, - }) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.requestImage$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - assetId, - requestId, - width, - height, - isVideo, - ]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Map?)!.cast(); - } - } - - Future cancelImageRequest(int requestId) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.cancelImageRequest$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } - - Future> getThumbhash(String thumbhash) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbhash$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([thumbhash]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Map?)!.cast(); - } - } -} diff --git a/mobile/lib/platform/view_intent_api.g.dart b/mobile/lib/platform/view_intent_api.g.dart deleted file mode 100644 index d457c249de..0000000000 --- a/mobile/lib/platform/view_intent_api.g.dart +++ /dev/null @@ -1,191 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -class ViewIntentPayload { - ViewIntentPayload({this.path, required this.mimeType, this.localAssetId}); - - String? path; - - String mimeType; - - String? localAssetId; - - List _toList() { - return [path, mimeType, localAssetId]; - } - - Object encode() { - return _toList(); - } - - static ViewIntentPayload decode(Object result) { - result as List; - return ViewIntentPayload( - path: result[0] as String?, - mimeType: result[1]! as String, - localAssetId: result[2] as String?, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! ViewIntentPayload || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(path, other.path) && - _deepEquals(mimeType, other.mimeType) && - _deepEquals(localAssetId, other.localAssetId); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is ViewIntentPayload) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - return ViewIntentPayload.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class ViewIntentHostApi { - /// Constructor for [ViewIntentHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - ViewIntentHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future consumeViewIntent() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ); - return pigeonVar_replyValue as ViewIntentPayload?; - } -} diff --git a/mobile/lib/presentation/actions/action.dart b/mobile/lib/presentation/actions/action.dart index 5ceb2f855d..d880b7ddf7 100644 --- a/mobile/lib/presentation/actions/action.dart +++ b/mobile/lib/presentation/actions/action.dart @@ -1,32 +1,56 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/asset_filter.dart'; -class ActionScope { - final BuildContext context; - final WidgetRef ref; - final UserDto authUser; +class ActionItem { + final IconData icon; + final String label; + final FutureOr Function() onAction; + final FutureOr Function()? onSecondaryAction; - const ActionScope({required this.context, required this.ref, required this.authUser}); + const ActionItem({required this.icon, required this.label, required this.onAction, this.onSecondaryAction}); } -abstract class BaseAction { - const BaseAction(); +abstract class ActionBuilder { + const ActionBuilder(); - IconData get icon; - - String label(ActionScope scope); - - bool isVisible(ActionScope scope) => true; - - Future onAction(ActionScope scope); + // null when the action is not applicable for the current context + ActionItem? create(BuildContext context, WidgetRef ref); } -abstract class AssetAction extends BaseAction { - final Iterable assets; +final assetsActionProvider = Provider.family.autoDispose, ActionSource>( + (ref, source) => AssetFilter(switch (source) { + .timeline => ref.watch(multiSelectProvider.select((s) => s.selectedAssets)), + .viewer => switch (ref.watch(assetViewerProvider.select((s) => s.currentAsset))) { + final BaseAsset asset => {asset}, + null => const {}, + }, + }), + dependencies: [multiSelectProvider], +); - const AssetAction({required this.assets}); +final clearSelectionProvider = Provider.family.autoDispose((ref, source) { + if (source == .timeline) { + return ref.read(multiSelectProvider.notifier).reset; + } - Iterable filter(ActionScope scope) => assets.whereType(); + return () {}; +}, dependencies: [multiSelectProvider]); + +final ownedAssetsActionProvider = Provider.family.autoDispose, ActionSource>( + (ref, source) => ref.watch(assetsActionProvider(source)).owned(ref.watch(authUserProvider).id), + dependencies: [assetsActionProvider], +); + +abstract class AssetActionBuilder extends ActionBuilder { + final ActionSource source; + + const AssetActionBuilder({required this.source}); } diff --git a/mobile/lib/presentation/actions/action.widget.dart b/mobile/lib/presentation/actions/action.widget.dart index eba5e3939c..a865528440 100644 --- a/mobile/lib/presentation/actions/action.widget.dart +++ b/mobile/lib/presentation/actions/action.widget.dart @@ -1,95 +1,71 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/utils/error_handler.dart'; import 'package:immich_ui/immich_ui.dart'; -class _ActionWidgetScope { - final String label; - final VoidCallback onAction; +abstract class ActionWidget extends ConsumerWidget { + final ActionBuilder action; - const _ActionWidgetScope({required this.label, required this.onAction}); -} + const ActionWidget({super.key, required this.action}); -class _ActionWidget extends ConsumerWidget { - final BaseAction action; - final Widget Function(_ActionWidgetScope context) builder; - - const _ActionWidget({required this.action, required this.builder}); - - Future _onAction(ActionScope scope) async { - try { - await action.onAction(scope); - } catch (error, stackTrace) { - handleError(scope.context, stack: stackTrace, description: 'Action failed: ${action.runtimeType}'); - } - } + Widget builder(BuildContext context, WidgetRef ref, ActionItem action); @override Widget build(BuildContext context, WidgetRef ref) { - final authUser = ref.watch(currentUserProvider); - if (authUser == null) { + final actionItem = action.create(context, ref); + if (actionItem == null) { return const SizedBox.shrink(); } - final scope = ActionScope(context: context, ref: ref, authUser: authUser); - if (!action.isVisible(scope)) { - return const SizedBox.shrink(); - } - - return builder(.new(label: action.label(scope), onAction: () => _onAction(scope))); + return builder(context, ref, actionItem); } } -class ActionIconButtonWidget extends StatelessWidget { - final BaseAction action; +class ActionColumnButton extends ActionWidget { + const ActionColumnButton({super.key, required super.action}); + + @override + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => ImmichColumnButton( + icon: action.icon, + label: action.label, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, + ); +} + +class ActionIconButton extends ActionWidget { final ImmichVariant variant; - const ActionIconButtonWidget({super.key, required this.action, this.variant = .ghost}); + const ActionIconButton({super.key, required super.action, this.variant = .ghost}); @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => ImmichIconButton(icon: action.icon, onPressed: ctx.onAction, variant: variant), + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => ImmichIconButton( + icon: action.icon, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, + variant: variant, ); } -class ActionButtonWidget extends StatelessWidget { - final BaseAction action; +class ActionButton extends ActionWidget { final ImmichVariant variant; - const ActionButtonWidget({super.key, required this.action, this.variant = .ghost}); + const ActionButton({super.key, required super.action, this.variant = .ghost}); @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => - ImmichTextButton(labelText: ctx.label, icon: action.icon, onPressed: ctx.onAction, variant: variant), + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => ImmichTextButton( + labelText: action.label, + icon: action.icon, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, + variant: variant, ); } -class ActionColumnButtonWidget extends StatelessWidget { - final BaseAction action; - - const ActionColumnButtonWidget({super.key, required this.action}); +class ActionMenuItem extends ActionWidget { + const ActionMenuItem({super.key, required super.action}); @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => ImmichColumnButton(icon: action.icon, label: ctx.label, onPressed: ctx.onAction), - ); -} - -class ActionMenuItemWidget extends StatelessWidget { - final BaseAction action; - - const ActionMenuItemWidget({super.key, required this.action}); - - @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => ImmichMenuItem(icon: action.icon, label: ctx.label, onPressed: ctx.onAction), - ); + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => + ImmichMenuItem(icon: action.icon, label: action.label, onPressed: action.onAction); } diff --git a/mobile/lib/presentation/actions/archive.action.dart b/mobile/lib/presentation/actions/archive.action.dart new file mode 100644 index 0000000000..f8e4a1e038 --- /dev/null +++ b/mobile/lib/presentation/actions/archive.action.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +typedef _State = ({bool shouldArchive, List assetIds}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + if (ref.watch(inLockedViewProvider)) { + return null; + } + + final assets = ref.watch(ownedAssetsActionProvider(source)); + final shouldArchive = assets.notVisibility(.archive).isNotEmpty; + final assetIds = assets + .visibility(shouldArchive ? .timeline : .archive) + .map((asset) => asset.id) + .toList(growable: false); + return assetIds.isEmpty ? null : (shouldArchive: shouldArchive, assetIds: assetIds); +}, dependencies: [ownedAssetsActionProvider]); + +class ArchiveAction extends AssetActionBuilder { + const ArchiveAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldArchive = ref.watch(_stateProvider(source).select((state) => state?.shouldArchive)); + if (shouldArchive == null) { + return null; + } + + return .new( + icon: shouldArchive ? Icons.archive_outlined : Icons.unarchive_outlined, + label: shouldArchive ? context.t.archive : context.t.unarchive, + onAction: () => _archive(context, ref), + ); + } + + Future _archive(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:shouldArchive, :assetIds) = state; + final message = shouldArchive + ? context.t.archive_action_prompt(count: assetIds.length) + : context.t.unarchive_action_prompt(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await assetService.update(assetIds, visibility: .some(shouldArchive ? .archive : .timeline)); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the archive status for assets"); + } + } +} diff --git a/mobile/lib/presentation/actions/asset_debug.action.dart b/mobile/lib/presentation/actions/asset_debug.action.dart index aec99fc90b..16ad15700e 100644 --- a/mobile/lib/presentation/actions/asset_debug.action.dart +++ b/mobile/lib/presentation/actions/asset_debug.action.dart @@ -1,27 +1,29 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -class AssetDebugAction extends AssetAction { - const AssetDebugAction({required super.assets}); +class AssetDebugAction extends AssetActionBuilder { + const AssetDebugAction({required super.source}); @override - IconData get icon => Icons.help_outline_rounded; + ActionItem? create(BuildContext context, WidgetRef ref) { + final asset = ref.watch(assetsActionProvider(source)).assets.singleOrNull; + final troubleshootEnabled = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); + if (!troubleshootEnabled || asset == null) { + return null; + } - @override - String label(ActionScope scope) => scope.context.t.troubleshoot; - - @override - bool isVisible(ActionScope scope) => - assets.length == 1 && scope.ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); - - @override - Future onAction(ActionScope scope) async => - unawaited(scope.context.pushRoute(AssetTroubleshootRoute(asset: assets.first))); + return .new( + icon: Icons.help_outline_rounded, + label: context.t.troubleshoot, + onAction: () => unawaited(context.pushRoute(AssetTroubleshootRoute(asset: asset))), + ); + } } diff --git a/mobile/lib/presentation/actions/cast.action.dart b/mobile/lib/presentation/actions/cast.action.dart new file mode 100644 index 0000000000..5d90479647 --- /dev/null +++ b/mobile/lib/presentation/actions/cast.action.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; + +class CastAction extends ActionBuilder { + const CastAction(); + + @override + ActionItem create(BuildContext context, WidgetRef ref) { + final isCasting = ref.watch(castProvider.select((state) => state.isCasting)); + + return .new( + icon: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + label: context.t.cast, + onAction: () async => unawaited(showDialog(context: context, builder: (_) => const CastDialog())), + ); + } +} diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart new file mode 100644 index 0000000000..31d01b32ef --- /dev/null +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/cleanup.service.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; + +typedef _State = ({List localIds, List remoteIds, bool trash}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final authUserId = ref.watch(authUserProvider).id; + + final localIds = []; + final ownedRemote = []; + for (final asset in assets) { + if (asset.localId case final localId?) { + localIds.add(localId); + } + if (asset case final RemoteAsset remote when remote.ownerId == authUserId) { + ownedRemote.add(remote); + } + } + + if (localIds.isEmpty && ownedRemote.isEmpty) { + return null; + } + + 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); + + return (localIds: localIds, remoteIds: ownedRemote.map((asset) => asset.id).toList(growable: false), trash: trash); +}, dependencies: [assetsActionProvider]); + +class DeleteAction extends AssetActionBuilder { + const DeleteAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final trash = ref.watch(_stateProvider(source).select((state) => state?.trash)); + if (trash == null) { + return null; + } + + return .new( + icon: Icons.delete_outline, + label: trash ? context.t.trash : context.t.delete, + onAction: () => _delete(context, ref), + ); + } + + Future _delete(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:localIds, :remoteIds, :trash) = state; + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final String? message; + if (remoteIds.isEmpty) { + message = await _removeLocalAssets(context, ref, localIds); + } else if (trash) { + message = await _moveToTrash(context, ref, remoteIds, localIds); + } else { + message = await _deletePermanently(context, ref, remoteIds, localIds); + } + + if (message == null) { + return; + } + + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to delete assets"); + } + } + + Future _removeLocalAssets(BuildContext context, WidgetRef ref, List localIds) async { + final count = await _cleanupLocalAssets(context, ref, localIds); + if (count <= 0 || !context.mounted) { + return null; + } + + return context.t.cleanup_deleted_assets(count: count); + } + + Future _moveToTrash( + BuildContext context, + WidgetRef ref, + List remoteIds, + List localIds, + ) async { + final assetService = ref.read(assetServiceProvider); + if (localIds.isNotEmpty) { + await _cleanupLocalAssets(context, ref, localIds); + if (!context.mounted) { + return null; + } + } + + final message = context.t.trash_action_prompt(count: remoteIds.length); + await assetService.trash(remoteIds); + return message; + } + + Future _deletePermanently( + BuildContext context, + WidgetRef ref, + List remoteIds, + List localIds, + ) async { + 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'), + ); + if (confirmed != true || !context.mounted) { + return null; + } + + final message = context.t.delete_permanently_action_prompt(count: remoteIds.length); + // Server first, so a failed request will not remove the local copy + await assetService.delete(remoteIds); + if (localIds.isNotEmpty && context.mounted) { + await _cleanupLocalAssets(context, ref, localIds, requestCustomPrompt: false); + } + + return message; + } +} + +final _cleanupStateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final assetIds = assets.backedUp().map((asset) => asset.localId).nonNulls.toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}, dependencies: [assetsActionProvider]); + +class CleanupLocalAction extends AssetActionBuilder { + const CleanupLocalAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final isVisible = ref.watch(_cleanupStateProvider(source).select((state) => state != null)); + if (!isVisible) { + return null; + } + + return .new( + icon: Icons.no_cell_outlined, + label: context.t.control_bottom_app_bar_delete_from_local, + onAction: () => _cleanup(context, ref), + ); + } + + Future _cleanup(BuildContext context, WidgetRef ref) async { + final assetIds = ref.read(_cleanupStateProvider(source)); + if (assetIds == null) { + return; + } + + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final count = await _cleanupLocalAssets(context, ref, assetIds); + if (count <= 0 || !context.mounted) { + return; + } + + toastService.success(context.t.cleanup_deleted_assets(count: count)); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to remove the device copies"); + } + } +} + +/// Removes the device copies of [assetIds], returning how many were deleted. +/// +/// iOS and Android without MANAGE_MEDIA prompt the user +/// with MANAGE_MEDIA, we do it ourselves unless [requestCustomPrompt] is false. +Future _cleanupLocalAssets( + BuildContext context, + WidgetRef ref, + List assetIds, { + bool requestCustomPrompt = true, +}) async { + if (assetIds.isEmpty) { + return 0; + } + + final cleanupService = ref.read(cleanupServiceProvider); + final requiresPrompt = + requestCustomPrompt && + CurrentPlatform.isAndroid && + ref.read(storeServiceProvider).get(.manageLocalMediaAndroid, false); + + if (requiresPrompt) { + final confirmed = await showDialog( + context: context, + builder: (_) => ConfirmDialog( + title: context.t.move_to_device_trash, + content: context.t.free_up_space_description, + ok: context.t.ok, + ), + ); + if (confirmed != true) { + return 0; + } + } + + return cleanupService.deleteLocalAssets(assetIds); +} diff --git a/mobile/lib/presentation/actions/download.action.dart b/mobile/lib/presentation/actions/download.action.dart new file mode 100644 index 0000000000..60a1395bcd --- /dev/null +++ b/mobile/lib/presentation/actions/download.action.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final remote = assets.remote().toList(growable: false); + return remote.isEmpty ? null : remote; +}, dependencies: [assetsActionProvider]); + +class DownloadAction extends AssetActionBuilder { + const DownloadAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assets = ref.watch(_stateProvider(source)); + if (assets == null) { + return null; + } + + return .new(icon: Icons.download, label: context.t.download, onAction: () => _download(ref, assets)); + } + + Future _download(WidgetRef ref, List assets) async { + final backgroundSync = ref.read(backgroundSyncProvider); + final downloads = ref.read(downloadRepositoryProvider); + + try { + await downloads.downloadAllAssets(assets); + + unawaited( + Future.delayed(const .new(seconds: 1), () async { + await backgroundSync.syncLocal(); + await backgroundSync.hashAssets(); + }), + ); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to download the assets"); + } + } +} diff --git a/mobile/lib/presentation/actions/edit_asset.action.dart b/mobile/lib/presentation/actions/edit_asset.action.dart new file mode 100644 index 0000000000..9b11032df9 --- /dev/null +++ b/mobile/lib/presentation/actions/edit_asset.action.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/utils/semver.dart'; + +const _minimumServerVersion = SemVer(major: 2, minor: 6, patch: 0); + +final _stateProvider = Provider.family.autoDispose((ref, source) { + final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion >= _minimumServerVersion)); + if (!isSupported) { + return null; + } + + final assets = ref.watch(ownedAssetsActionProvider(source)); + return assets.where((asset) => asset.isEditable).singleOrNull; +}, dependencies: [ownedAssetsActionProvider]); + +class EditAssetAction extends AssetActionBuilder { + const EditAssetAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((asset) => asset != null))) { + return null; + } + + return .new(icon: Icons.tune, label: context.t.edit, onAction: () => _edit(context, ref)); + } + + Future _edit(BuildContext context, WidgetRef ref) async { + final asset = ref.read(_stateProvider(source)); + if (asset == null) { + return; + } + + try { + // TODO(shenlong): Move all EXIF and Apply Edits logic onto the Route + final repository = ref.read(remoteAssetRepositoryProvider); + final (edits, exif) = await (repository.getAssetEdits(asset.id), repository.getExif(asset.id)).wait; + if (exif == null || !context.mounted) { + return; + } + + ref.read(editorStateProvider.notifier).init(edits, exif); + unawaited( + context.pushRoute( + DriftEditImageRoute( + image: Image(image: getFullImageProvider(asset, edited: false)), + applyEdits: (newEdits) => applyEdits(ref, asset.id, newEdits), + ), + ), + ); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to open the editor for the asset"); + } + } +} + +@visibleForTesting +Future applyEdits(WidgetRef ref, String remoteId, List edits) async { + final websocket = ref.read(websocketProvider.notifier); + + bool isCurrentId(dynamic data) => data is Map && (data['asset'] as Map?)?['id'] == remoteId; + await ref.read(assetServiceProvider).applyEdits(remoteId, edits); + await Future.any([ + websocket.waitForEvent('AssetEditReadyV1', isCurrentId, const .new(seconds: 10)), + websocket.waitForEvent('AssetEditReadyV2', isCurrentId, const .new(seconds: 10)), + ]).catchError((_) {}); +} diff --git a/mobile/lib/presentation/actions/edit_datetime.action.dart b/mobile/lib/presentation/actions/edit_datetime.action.dart new file mode 100644 index 0000000000..31de23d1a7 --- /dev/null +++ b/mobile/lib/presentation/actions/edit_datetime.action.dart @@ -0,0 +1,100 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/utils/timezone.dart'; +import 'package:immich_mobile/widgets/common/date_time_picker.dart'; + +typedef _State = ({List assetIds, RemoteAsset? origin}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull); +}, dependencies: [ownedAssetsActionProvider]); + +class EditDateTimeAction extends AssetActionBuilder { + const EditDateTimeAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((state) => state != null))) { + return null; + } + + return .new( + icon: Icons.edit_calendar_outlined, + label: context.t.control_bottom_app_bar_edit_time, + onAction: () => _edit(context, ref), + ); + } + + Future _edit(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:assetIds, :origin) = state; + final remoteAssetRepository = ref.read(remoteAssetRepositoryProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + DateTime? initialDate; + String? timeZone; + Duration? offset; + + if (origin != null) { + final exif = await remoteAssetRepository.getExif(origin.id); + + // Prefer the EXIF timezone, so the picker opens on what the asset actually shows. + DateTime dateTime = origin.createdAt.toLocal(); + offset = dateTime.timeZoneOffset; + if (exif?.dateTimeOriginal case final original?) { + timeZone = exif!.timeZone; + (dateTime, offset) = applyTimezoneOffset(dateTime: original, timeZone: exif.timeZone); + } + initialDate = dateTime; + + if (!context.mounted) { + return; + } + } + + final picked = await showDateTimePicker( + context: context, + initialDateTime: initialDate, + initialTZ: timeZone, + initialTZOffset: offset, + ); + if (picked == null || !context.mounted) { + return; + } + + await saveDateTime(context, ref, assetIds, picked); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the date and time for assets"); + } + } +} + +@visibleForTesting +Future saveDateTime(BuildContext context, WidgetRef ref, List assetIds, String dateTime) async { + final message = context.t.edit_date_and_time_action_prompt(count: assetIds.length); + final toastService = ref.read(toastServiceProvider); + + await ref.read(assetServiceProvider).update(assetIds, dateTime: .some(dateTime)); + ref.invalidate(assetExifProvider); + toastService.success(message); +} diff --git a/mobile/lib/presentation/actions/edit_location.action.dart b/mobile/lib/presentation/actions/edit_location.action.dart new file mode 100644 index 0000000000..5ce74a0f40 --- /dev/null +++ b/mobile/lib/presentation/actions/edit_location.action.dart @@ -0,0 +1,85 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/widgets/common/location_picker.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +typedef _State = ({List assetIds, RemoteAsset? origin}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull); +}, dependencies: [ownedAssetsActionProvider]); + +class EditLocationAction extends AssetActionBuilder { + const EditLocationAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((state) => state != null))) { + return null; + } + + return .new( + icon: Icons.edit_location_alt_outlined, + label: context.t.control_bottom_app_bar_edit_location, + onAction: () => _edit(context, ref), + ); + } + + Future _edit(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:assetIds, :origin) = state; + final remoteAssetRepository = ref.read(remoteAssetRepositoryProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + LatLng? initialLatLng; + if (origin != null) { + final exif = await remoteAssetRepository.getExif(origin.id); + if (exif?.latitude != null && exif?.longitude != null) { + initialLatLng = LatLng(exif!.latitude!, exif.longitude!); + } + if (!context.mounted) { + return; + } + } + + final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); + if (location == null || !context.mounted) { + return; + } + + await saveLocation(context, ref, assetIds, location); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the location for assets"); + } + } +} + +@visibleForTesting +Future saveLocation(BuildContext context, WidgetRef ref, List assetIds, LatLng location) async { + final message = context.t.edit_location_action_prompt(count: assetIds.length); + final toastService = ref.read(toastServiceProvider); + + await ref.read(assetServiceProvider).update(assetIds, location: .some(location)); + ref.invalidate(assetExifProvider); + toastService.success(message); +} diff --git a/mobile/lib/presentation/actions/favorite.action.dart b/mobile/lib/presentation/actions/favorite.action.dart index a480e10a5f..402d2f3833 100644 --- a/mobile/lib/presentation/actions/favorite.action.dart +++ b/mobile/lib/presentation/actions/favorite.action.dart @@ -1,38 +1,62 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/utils/asset_filter.dart'; -import 'package:immich_ui/immich_ui.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; -class FavoriteAction extends AssetAction { - final bool favorite; +typedef _State = ({bool shouldFavorite, List assetIds}); - FavoriteAction({required super.assets}) : favorite = assets.any((asset) => !asset.isFavorite); +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + final shouldFavorite = assets.favorite(isFavorite: false).isNotEmpty; + final assetIds = assets.favorite(isFavorite: !shouldFavorite).map((asset) => asset.id).toList(growable: false); + return (shouldFavorite: shouldFavorite, assetIds: assetIds); +}, dependencies: [ownedAssetsActionProvider]); + +class FavoriteAction extends AssetActionBuilder { + const FavoriteAction({required super.source}); @override - IconData get icon => favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded; + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldFavorite = ref.watch(_stateProvider(source).select((state) => state?.shouldFavorite)); + if (shouldFavorite == null) { + return null; + } - @override - String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite; + return .new( + icon: shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, + label: shouldFavorite ? context.t.favorite : context.t.unfavorite, + onAction: () => _favorite(context, ref), + ); + } - @override - Iterable filter(ActionScope scope) => - AssetFilter(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite); + Future _favorite(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } - @override - bool isVisible(ActionScope scope) => filter(scope).isNotEmpty; + final (:shouldFavorite, :assetIds) = state; + final message = shouldFavorite + ? context.t.favorite_action_prompt(count: assetIds.length) + : context.t.unfavorite_action_prompt(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); - @override - Future onAction(ActionScope scope) async { - final ActionScope(:ref) = scope; - final assets = filter(scope).map((asset) => asset.id).toList(growable: false); - - await ref.read(assetServiceProvider).updateFavorite(assets, favorite); - final message = favorite - ? StaticTranslations.instance.favorite_action_prompt(count: assets.length) - : StaticTranslations.instance.unfavorite_action_prompt(count: assets.length); - snackbar.success(message); + try { + await assetService.update(assetIds, isFavorite: .some(shouldFavorite)); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update favorite status for assets"); + } } } diff --git a/mobile/lib/presentation/actions/lock.action.dart b/mobile/lib/presentation/actions/lock.action.dart new file mode 100644 index 0000000000..3d090b712d --- /dev/null +++ b/mobile/lib/presentation/actions/lock.action.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +typedef _State = ({bool shouldLock, List assetIds, List localIds}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + final shouldLock = assets.locked(isLocked: false).isNotEmpty; + final targets = assets.locked(isLocked: !shouldLock); + return ( + shouldLock: shouldLock, + assetIds: targets.map((asset) => asset.id).toList(growable: false), + // Only locking has an on-device copy to clean up; unlocking leaves the device alone. + localIds: shouldLock ? targets.map((asset) => asset.localId).nonNulls.toList(growable: false) : const [], + ); +}, dependencies: [ownedAssetsActionProvider]); + +class LockAction extends AssetActionBuilder { + const LockAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldLock = ref.watch(_stateProvider(source).select((state) => state?.shouldLock)); + if (shouldLock == null) { + return null; + } + + return .new( + icon: shouldLock ? Icons.lock_rounded : Icons.lock_open_rounded, + label: shouldLock ? context.t.move_to_locked_folder : context.t.remove_from_locked_folder, + onAction: () => _lock(context, ref), + ); + } + + Future _lock(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:shouldLock, :assetIds, :localIds) = state; + final message = shouldLock + ? context.t.move_to_lock_folder_action_prompt(count: assetIds.length) + : context.t.remove_from_lock_folder_action_prompt(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await assetService.update(assetIds, visibility: .some(shouldLock ? .locked : .timeline)); + if (localIds.isNotEmpty) { + // A locked asset still sits in the device gallery, so offer to remove the local copy. + await assetService.deleteLocal(localIds); + } + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the locked folder for assets"); + } + } +} diff --git a/mobile/lib/presentation/actions/open_in_browser.action.dart b/mobile/lib/presentation/actions/open_in_browser.action.dart new file mode 100644 index 0000000000..6b05762977 --- /dev/null +++ b/mobile/lib/presentation/actions/open_in_browser.action.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class OpenInBrowserAction extends ActionBuilder { + final String remoteId; + final TimelineOrigin origin; + + const OpenInBrowserAction({required this.remoteId, required this.origin}); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => + .new(icon: Icons.open_in_browser, label: context.t.open_in_browser, onAction: () => _open(ref)); + + Future _open(WidgetRef ref) async { + final serverEndpoint = ref.read(storeServiceProvider).get(.serverEndpoint).replaceFirst('/api', ''); + final url = Uri.parse('$serverEndpoint${webPathFor(origin)}/photos/$remoteId'); + + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: .externalApplication); + } + } +} + +@visibleForTesting +String webPathFor(TimelineOrigin origin) => switch (origin) { + .favorite => '/favorites', + .trash => '/trash', + .archive => '/archive', + _ => '', +}; diff --git a/mobile/lib/presentation/actions/partner.action.dart b/mobile/lib/presentation/actions/partner.action.dart index 11fb69ee75..012340c1d8 100644 --- a/mobile/lib/presentation/actions/partner.action.dart +++ b/mobile/lib/presentation/actions/partner.action.dart @@ -6,44 +6,46 @@ import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -class PartnerAddAction extends BaseAction { +class PartnerAddAction extends ActionBuilder { const PartnerAddAction(); @override - IconData get icon => Icons.person_add_rounded; + ActionItem create(BuildContext context, WidgetRef ref) => + ActionItem(icon: Icons.person_add_rounded, label: context.t.add_partner, onAction: () => _add(context, ref)); - @override - String label(ActionScope scope) => scope.context.t.add_partner; + Future _add(BuildContext context, WidgetRef ref) async { + final partnerService = ref.read(partnerServiceProvider); + final authUserId = ref.read(authUserProvider).id; - @override - Future onAction(ActionScope scope) async { - final ActionScope(:context, :ref, :authUser) = scope; final selected = await showDialog(context: context, builder: (_) => const PartnerSelectionDialog()); if (selected == null) { return; } - await ref.read(partnerServiceProvider).create(sharedById: authUser.id, sharedWithId: selected.id); + try { + await partnerService.create(sharedById: authUserId, sharedWithId: selected.id); + } catch (error, stack) { + handleError(error, stack: stack, description: 'Failed to add partner'); + } } } -class PartnerRemoveAction extends BaseAction { +class PartnerRemoveAction extends ActionBuilder { const PartnerRemoveAction({required this.sharedWithId, required this.partnerName}); final String sharedWithId; final String partnerName; @override - IconData get icon => Icons.person_remove_rounded; + ActionItem create(BuildContext context, WidgetRef ref) => + ActionItem(icon: Icons.person_remove_rounded, label: context.t.remove, onAction: () => _remove(context, ref)); - @override - String label(ActionScope scope) => scope.context.t.remove; - - @override - Future onAction(ActionScope scope) async { - final ActionScope(:context, :ref, :authUser) = scope; + Future _remove(BuildContext context, WidgetRef ref) async { + final partnerService = ref.read(partnerServiceProvider); + final authUserId = ref.read(authUserProvider).id; final confirmed = await showDialog( context: context, @@ -56,19 +58,18 @@ class PartnerRemoveAction extends BaseAction { return; } - await ref.read(partnerServiceProvider).delete(sharedById: authUser.id, sharedWithId: sharedWithId); + try { + await partnerService.delete(sharedById: authUserId, sharedWithId: sharedWithId); + } catch (error, stack) { + handleError(error, stack: stack, description: 'Failed to remove partner'); + } } } @visibleForTesting -final candidatesStateProvider = StreamProvider.autoDispose>((ref) { - final currentUser = ref.watch(currentUserProvider); - // TODO: Refactor with a route guard to avoid this check in every provider - if (currentUser == null) { - return const Stream.empty(); - } - return ref.watch(partnerServiceProvider).getCandidates(currentUser.id); -}); +final candidatesStateProvider = StreamProvider.autoDispose>( + (ref) => ref.watch(partnerServiceProvider).getCandidates(ref.watch(authUserProvider).id), +); @visibleForTesting class PartnerSelectionDialog extends ConsumerWidget { diff --git a/mobile/lib/presentation/actions/remove_from_album.action.dart b/mobile/lib/presentation/actions/remove_from_album.action.dart new file mode 100644 index 0000000000..9e2d9d582f --- /dev/null +++ b/mobile/lib/presentation/actions/remove_from_album.action.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final assetIds = assets.remote().map((asset) => asset.id).toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}, dependencies: [assetsActionProvider]); + +class RemoveFromAlbumAction extends AssetActionBuilder { + final String albumId; + + const RemoveFromAlbumAction({required super.source, required this.albumId}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assetIds = ref.watch(_stateProvider(source)); + if (assetIds == null) { + return null; + } + + return .new( + icon: Icons.remove_circle_outline, + label: context.t.remove_from_album, + onAction: () => _remove(context, ref, assetIds), + ); + } + + Future _remove(BuildContext context, WidgetRef ref, List assetIds) async { + final albumService = ref.read(remoteAlbumServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final count = await albumService.removeAssets(albumId: albumId, assetIds: assetIds); + if (!context.mounted) { + return; + } + + toastService.success(context.t.remove_from_album_action_prompt(count: count)); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to remove the assets from the album"); + } + } +} diff --git a/mobile/lib/presentation/actions/restore.action.dart b/mobile/lib/presentation/actions/restore.action.dart new file mode 100644 index 0000000000..0a1b707b39 --- /dev/null +++ b/mobile/lib/presentation/actions/restore.action.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + final assetIds = assets.trashed().map((asset) => asset.id).toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}, dependencies: [ownedAssetsActionProvider]); + +class RestoreAction extends AssetActionBuilder { + const RestoreAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((state) => state != null))) { + return null; + } + + return .new(icon: Icons.history_rounded, label: context.t.restore, onAction: () => _restore(context, ref)); + } + + Future _restore(BuildContext context, WidgetRef ref) async { + final assetIds = ref.read(_stateProvider(source)); + if (assetIds == null) { + return; + } + + final message = context.t.assets_restored_count(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await assetService.restoreTrash(assetIds); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to restore assets"); + } + } +} diff --git a/mobile/lib/presentation/actions/set_album_cover.action.dart b/mobile/lib/presentation/actions/set_album_cover.action.dart new file mode 100644 index 0000000000..d5ad367eed --- /dev/null +++ b/mobile/lib/presentation/actions/set_album_cover.action.dart @@ -0,0 +1,49 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +final _stateProvider = Provider.family.autoDispose((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + return assets.remote().map((asset) => asset.id).singleOrNull; +}, dependencies: [assetsActionProvider]); + +class SetAlbumCoverAction extends AssetActionBuilder { + final String albumId; + + const SetAlbumCoverAction({required super.source, required this.albumId}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assetId = ref.watch(_stateProvider(source)); + if (assetId == null) { + return null; + } + + return .new( + icon: Icons.image_outlined, + label: context.t.set_as_album_cover, + onAction: () => _setCover(context, ref, assetId), + ); + } + + Future _setCover(BuildContext context, WidgetRef ref, String assetId) async { + final message = context.t.album_cover_updated; + final albumService = ref.read(remoteAlbumServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await albumService.updateAlbum(albumId, thumbnailAssetId: assetId); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the album cover"); + } + } +} diff --git a/mobile/lib/presentation/actions/set_profile_picture.action.dart b/mobile/lib/presentation/actions/set_profile_picture.action.dart new file mode 100644 index 0000000000..c14be1dc87 --- /dev/null +++ b/mobile/lib/presentation/actions/set_profile_picture.action.dart @@ -0,0 +1,22 @@ +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/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SetProfilePictureAction extends ActionBuilder { + final BaseAsset asset; + + const SetProfilePictureAction({required this.asset}); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => .new( + icon: Icons.account_circle_outlined, + label: context.t.set_as_profile_picture, + onAction: () async => unawaited(context.pushRoute(ProfilePictureCropRoute(asset: asset))), + ); +} diff --git a/mobile/lib/presentation/actions/share.action.dart b/mobile/lib/presentation/actions/share.action.dart new file mode 100644 index 0000000000..3b8a6318de --- /dev/null +++ b/mobile/lib/presentation/actions/share.action.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final shareable = assets.toList(growable: false); + return shareable.isEmpty ? null : shareable; +}, dependencies: [assetsActionProvider]); + +class ShareAction extends AssetActionBuilder { + const ShareAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assets = ref.watch(_stateProvider(source)); + if (assets == null) { + return null; + } + + return .new( + icon: CurrentPlatform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, + label: context.t.share, + onAction: () => _share(context, ref, assets, ref.read(appConfigProvider).share.fileType), + onSecondaryAction: () => _promptQualityAndShare(context, ref, assets), + ); + } + + Future _promptQualityAndShare(BuildContext context, WidgetRef ref, List assets) async { + // Only show preview option when at least one of the assets is not a video + final showPreview = assets.any((asset) => !asset.isVideo); + + final fileType = await showDialog( + context: context, + builder: (_) => _ShareFileTypeDialog(showPreview: showPreview), + useRootNavigator: false, + ); + if (fileType == null || !context.mounted) { + return; + } + + await _share(context, ref, assets, fileType); + } + + Future _share(BuildContext context, WidgetRef ref, List assets, ShareAssetType fileType) async { + final cancelCompleter = Completer(); + final progress = ValueNotifier(null); + final mediaRepository = ref.read(assetMediaRepositoryProvider); + final toastService = ref.read(toastServiceProvider); + final errorMessage = context.t.scaffold_body_error_occurred; + + await showDialog( + context: context, + barrierDismissible: false, + useRootNavigator: false, + builder: (dialogContext) { + void finish({required bool failed}) { + if (cancelCompleter.isCompleted || !dialogContext.mounted) { + return; + } + if (failed) { + unawaited(.value(toastService.error(errorMessage))); + } + dialogContext.pop(); + } + + unawaited( + mediaRepository + .shareAssets( + assets, + context, + fileType: fileType, + cancelCompleter: cancelCompleter, + onAssetDownloadProgress: (value) => progress.value = value, + ) + .then((count) => finish(failed: count == 0), onError: (_) => finish(failed: true)), + ); + + return _SharePreparingDialog(progress: progress); + }, + ).then((_) { + if (!cancelCompleter.isCompleted) { + cancelCompleter.complete(); + } + progress.dispose(); + }); + } +} + +class _SharePreparingDialog extends StatelessWidget { + final ValueNotifier progress; + + const _SharePreparingDialog({required this.progress}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: Column( + mainAxisSize: .min, + children: [ + Container(margin: const .only(bottom: 12), child: Text(context.t.share_dialog_preparing)), + SizedBox( + width: 240, + child: ValueListenableBuilder( + valueListenable: progress, + builder: (context, value, _) { + final percent = value == null ? null : (value * 100).clamp(0, 100); + return Column( + mainAxisSize: .min, + children: [ + LinearProgressIndicator(value: value, minHeight: 8.0), + if (percent != null) + Container(margin: const .only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')), + ], + ); + }, + ), + ), + ], + ), + ); + } +} + +class _ShareFileTypeDialog extends StatelessWidget { + final bool showPreview; + + const _ShareFileTypeDialog({this.showPreview = true}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(context.t.select_quality), + contentPadding: const .symmetric(vertical: 8), + content: Column( + mainAxisSize: .min, + children: [ + ListTile( + leading: const Icon(Icons.high_quality_rounded), + title: Text(context.t.share_original), + onTap: () => context.pop(ShareAssetType.original), + ), + if (showPreview) + ListTile( + leading: const Icon(Icons.photo_size_select_large_rounded), + title: Text(context.t.share_preview), + onTap: () => context.pop(ShareAssetType.preview), + ), + ], + ), + actions: [TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel))], + ); + } +} diff --git a/mobile/lib/presentation/actions/share_link.action.dart b/mobile/lib/presentation/actions/share_link.action.dart new file mode 100644 index 0000000000..4285105ed3 --- /dev/null +++ b/mobile/lib/presentation/actions/share_link.action.dart @@ -0,0 +1,33 @@ +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/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/routing/router.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final remoteIds = assets.remote().map((asset) => asset.id).toList(growable: false); + return remoteIds.isEmpty ? null : remoteIds; +}, dependencies: [assetsActionProvider]); + +class ShareLinkAction extends AssetActionBuilder { + const ShareLinkAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final remoteIds = ref.watch(_stateProvider(source)); + if (remoteIds == null) { + return null; + } + + return .new( + icon: Icons.link_rounded, + label: context.t.share_link, + onAction: () async => unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))), + ); + } +} diff --git a/mobile/lib/presentation/actions/similar_photos.action.dart b/mobile/lib/presentation/actions/similar_photos.action.dart new file mode 100644 index 0000000000..3c0b7b129b --- /dev/null +++ b/mobile/lib/presentation/actions/similar_photos.action.dart @@ -0,0 +1,42 @@ +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/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SimilarPhotosAction extends ActionBuilder { + final String assetId; + + const SimilarPhotosAction({required this.assetId}); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => + .new(icon: Icons.compare, label: context.t.view_similar_photos, onAction: () => _search(context, ref)); + + Future _search(BuildContext context, WidgetRef ref) async { + ref.invalidate(assetViewerProvider); + ref.invalidate(paginatedSearchProvider); + + ref.read(searchPreFilterProvider.notifier) + ..clear() + ..setFilter( + .new( + assetId: assetId, + people: {}, + location: .new(), + camera: .new(), + date: .new(), + display: .new(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: .new(), + mediaType: .other, + ), + ); + + unawaited(context.navigateTo(const DriftSearchRoute())); + } +} diff --git a/mobile/lib/presentation/actions/slideshow.action.dart b/mobile/lib/presentation/actions/slideshow.action.dart new file mode 100644 index 0000000000..4b6c90b517 --- /dev/null +++ b/mobile/lib/presentation/actions/slideshow.action.dart @@ -0,0 +1,21 @@ +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/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SlideshowAction extends ActionBuilder { + const SlideshowAction(); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => .new( + icon: Icons.slideshow, + label: context.t.slideshow, + onAction: () async => + unawaited(context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider)))), + ); +} diff --git a/mobile/lib/presentation/actions/stack.action.dart b/mobile/lib/presentation/actions/stack.action.dart new file mode 100644 index 0000000000..5ab978a7e4 --- /dev/null +++ b/mobile/lib/presentation/actions/stack.action.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +typedef _State = ({bool shouldStack, List assetIds, List stackIds}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + final shouldStack = assets.stacked(isStacked: false).isNotEmpty; + // Stacking needs at least two assets; unstacking needs at least one stack. + if (shouldStack ? assets.elementAtOrNull(1) == null : assets.isEmpty) { + return null; + } + + return ( + shouldStack: shouldStack, + assetIds: assets.map((asset) => asset.id).toList(growable: false), + stackIds: assets.map((asset) => asset.stackId).nonNulls.toList(growable: false), + ); +}, dependencies: [ownedAssetsActionProvider]); + +class StackAction extends AssetActionBuilder { + const StackAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldStack = ref.watch(_stateProvider(source).select((state) => state?.shouldStack)); + if (shouldStack == null) { + return null; + } + + return .new( + icon: shouldStack ? Icons.filter_none_rounded : Icons.layers_clear_outlined, + label: shouldStack ? context.t.stack : context.t.unstack, + onAction: () => _stack(context, ref), + ); + } + + Future _stack(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:shouldStack, :assetIds, :stackIds) = state; + final message = shouldStack + ? context.t.stacked_assets_count(count: assetIds.length) + : context.t.unstacked_assets_count(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final userId = ref.read(authUserProvider).id; + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + if (shouldStack) { + await assetService.stack(userId, assetIds); + } else { + await assetService.unstack(stackIds); + } + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the stack for assets"); + } + } +} diff --git a/mobile/lib/presentation/actions/tag.action.dart b/mobile/lib/presentation/actions/tag.action.dart new file mode 100644 index 0000000000..978648708f --- /dev/null +++ b/mobile/lib/presentation/actions/tag.action.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/services/tag.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/widgets/common/tag_picker.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final tagsEnabled = ref.watch( + userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), + ); + if (!tagsEnabled) { + return null; + } + + final assets = ref.watch(ownedAssetsActionProvider(source)); + final assetIds = assets.map((asset) => asset.id).toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}, dependencies: [ownedAssetsActionProvider]); + +class TagAction extends AssetActionBuilder { + const TagAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assetIds = ref.watch(_stateProvider(source)); + if (assetIds == null) { + return null; + } + + return .new( + icon: Icons.sell_outlined, + label: context.t.control_bottom_app_bar_add_tags, + onAction: () => _tag(context, ref, assetIds), + ); + } + + Future _tag(BuildContext context, WidgetRef ref, List assetIds) async { + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final results = await showTagPickerModal(context: context); + if (results == null || !context.mounted) { + return; + } + + final (selected, created) = results; + await tagAssets(context, ref, assetIds, selected: selected, created: created); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to tag the assets"); + } + } +} + +@visibleForTesting +Future tagAssets( + BuildContext context, + WidgetRef ref, + List assetIds, { + required Set selected, + required Set created, +}) async { + final tagService = ref.read(tagServiceProvider); + final toastService = ref.read(toastServiceProvider); + final tagIds = {...selected}; + + if (created.isNotEmpty) { + final tags = await tagService.upsertTags(created.toList()); + tagIds.addAll(tags.map((tag) => tag.id)); + } + if (tagIds.isEmpty) { + return; + } + + final count = await tagService.bulkTagAssets(assetIds, tagIds.toList()); + ref.invalidate(tagProvider); + if (context.mounted) { + toastService.success(context.t.tagged_assets(count: count)); + } +} diff --git a/mobile/lib/presentation/actions/timeline.action.dart b/mobile/lib/presentation/actions/timeline.action.dart deleted file mode 100644 index d8d367f674..0000000000 --- a/mobile/lib/presentation/actions/timeline.action.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/presentation/actions/action.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; - -class TimelineAction extends BaseAction { - final BaseAction action; - - const TimelineAction({required this.action}); - - @override - IconData get icon => action.icon; - - @override - String label(ActionScope scope) => action.label(scope); - - @override - bool isVisible(ActionScope scope) => action.isVisible(scope); - - @override - Future onAction(ActionScope scope) async { - await action.onAction(scope); - scope.ref.read(multiSelectProvider.notifier).reset(); - } -} diff --git a/mobile/lib/presentation/actions/upload.action.dart b/mobile/lib/presentation/actions/upload.action.dart new file mode 100644 index 0000000000..e52659beb7 --- /dev/null +++ b/mobile/lib/presentation/actions/upload.action.dart @@ -0,0 +1,145 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_ui/immich_ui.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final local = assets.backedUp(isBackedUp: false).local().toList(growable: false); + return local.isEmpty ? null : local; +}, dependencies: [assetsActionProvider]); + +class UploadAction extends AssetActionBuilder { + final bool showProgress; + + const UploadAction({required super.source, this.showProgress = false}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assets = ref.watch(_stateProvider(source)); + if (assets == null) { + return null; + } + + return .new(icon: Icons.backup_outlined, label: context.t.upload, onAction: () => _upload(context, ref, assets)); + } + + Future _upload(BuildContext context, WidgetRef ref, List assets) async { + try { + if (!showProgress) { + await uploadAssets(context, ref, assets); + return; + } + + // The dialog is not awaited: it stays up while the upload runs and is + // dismissed below, unless the user cancelled it themselves first + var isDialogOpen = true; + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const _UploadProgressDialog(), + ).whenComplete(() => isDialogOpen = false), + ); + + await uploadAssets(context, ref, assets); + + if (isDialogOpen && context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to upload the assets"); + } + } +} + +@visibleForTesting +Future uploadAssets(BuildContext context, WidgetRef ref, List assets) async { + final progress = ref.read(assetUploadProgressProvider.notifier); + final uploads = ref.read(foregroundUploadServiceProvider); + final toastService = ref.read(toastServiceProvider); + final errorMessage = context.t.scaffold_body_error_occurred; + + final cancelToken = Completer(); + ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken; + + final uploaded = {}; + final failed = {}; + for (final asset in assets) { + progress.setProgress(asset.id, 0.0); + } + + try { + await uploads.uploadManual( + assets, + cancelToken: cancelToken, + callbacks: UploadCallbacks( + onProgress: (id, _, bytes, total) => progress.setProgress(id, total > 0 ? bytes / total : 0.0), + onSuccess: (id, _) { + uploaded.add(id); + progress.remove(id); + }, + onError: (id, _) { + failed.add(id); + progress.setError(id); + }, + ), + ); + } finally { + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + } + + final uploadedCount = uploaded.difference(failed).length; + if (!cancelToken.isCompleted && (uploadedCount != assets.length || failed.isNotEmpty)) { + toastService.error(errorMessage); + } + + unawaited(Future.delayed(const Duration(seconds: 2), progress.clear)); +} + +class _UploadProgressDialog extends ConsumerWidget { + const _UploadProgressDialog(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final progressMap = ref.watch(assetUploadProgressProvider); + + final values = progressMap.values.where((value) => value >= 0).toList(growable: false); + final progress = values.isEmpty ? 0.0 : values.reduce((a, b) => a + b) / values.length; + final hasError = progressMap.values.any((value) => value < 0); + + return AlertDialog( + title: Text(context.t.uploading), + content: Column( + mainAxisSize: .min, + children: [ + if (hasError) + const Icon(Icons.error_outline, color: Colors.red, size: 48) + else + CircularProgressIndicator(value: progress > 0 ? progress : null), + const SizedBox(height: 16), + Text(hasError ? context.t.scaffold_body_error_occurred : '${(progress * 100).toInt()}%'), + ], + ), + actions: [ + ImmichTextButton( + onPressed: () { + ref.read(manualUploadCancelTokenProvider)?.complete(); + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + Navigator.of(context, rootNavigator: true).pop(); + }, + labelText: context.t.cancel, + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index b78bbc2979..f2215d169c 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -1,9 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_dialog.widget.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; -import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_dialog.widget.dart'; import 'package:immich_mobile/providers/feature_message.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; diff --git a/mobile/lib/presentation/pages/download_info.page.dart b/mobile/lib/presentation/pages/download_info.page.dart index e805458e76..8073d3098c 100644 --- a/mobile/lib/presentation/pages/download_info.page.dart +++ b/mobile/lib/presentation/pages/download_info.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,14 +16,14 @@ class DownloadInfoPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); - onCancelDownload(String id) { - ref.watch(downloadStateProvider.notifier).cancelDownload(id); + void onCancelDownload(String id) { + unawaited(ref.read(downloadStateProvider.notifier).cancelDownload(id)); } return Scaffold( appBar: AppBar( title: Text("download".t(context: context)), - actions: [], + actions: const [], ), body: ListView.builder( physics: const ClampingScrollPhysics(), diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index b998e10dc2..2bb3507e6e 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -21,17 +23,21 @@ class DriftActivitiesPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final activityNotifier = ref.read(albumActivityProvider((album.id, assetId)).notifier); + final activityNotifier = ref.watch(albumActivityProvider((album.id, assetId)).notifier); final activities = ref.watch(albumActivityProvider((album.id, assetId))); final listViewScrollController = useScrollController(); - void scrollToBottom() { - listViewScrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.fastOutSlowIn); + Future scrollToBottom() { + return listViewScrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + ); } Future onAddComment(String comment) async { await activityNotifier.addComment(comment); - scrollToBottom(); + unawaited(scrollToBottom()); } return ProviderScope( @@ -45,7 +51,7 @@ class DriftActivitiesPage extends HookConsumerWidget { if (assetName != null) Text(assetName!, style: context.textTheme.bodySmall), ], ), - actions: [const LikeActivityActionButton(iconOnly: true)], + actions: const [LikeActivityActionButton(iconOnly: true)], actionsPadding: const EdgeInsets.only(right: 8), ), body: activities.widgetWhen( @@ -71,7 +77,7 @@ class DriftActivitiesPage extends HookConsumerWidget { ), Align( alignment: Alignment.bottomCenter, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: context.scaffoldBackgroundColor, border: Border(top: BorderSide(color: context.colorScheme.secondaryContainer, width: 1)), diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 47a4625f87..ab91b6f3c8 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -53,7 +53,7 @@ class _DriftAlbumsPageState extends ConsumerState { ), AlbumSelector( onAlbumSelected: (album) { - context.router.push(RemoteAlbumRoute(album: album)); + unawaited(context.router.push(RemoteAlbumRoute(album: album))); }, ), ], diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 1a516426b5..7b4c48a6b0 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -43,16 +43,20 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ); } - void leaveAlbum() async { + Future leaveAlbum() async { try { await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId); + if (!context.mounted) { + return; + } + unawaited(context.navigateTo(const DriftAlbumsRoute())); } catch (_) { showErrorMessage(); } } - void removeUserFromAlbum(UserDto user) async { + Future removeUserFromAlbum(UserDto user) async { try { await ref.read(remoteAlbumProvider.notifier).removeUser(album.id, user.id); ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); @@ -71,23 +75,27 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { } try { - await ref.read(remoteAlbumProvider.notifier).addUsers(album.id, newUsers); - - if (newUsers.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), - toastType: ToastType.success, - ); + if (!context.mounted) { + return; } + await ref.read(remoteAlbumProvider.notifier).addUsers(album.id, newUsers); ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); - } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, - msg: "Failed to add users to album: ${e.toString()}", - toastType: ToastType.error, + msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + toastType: ToastType.success, ); + } catch (e) { + if (!context.mounted) { + return; + } + + ImmichToast.show(context: context, msg: "Failed to add users to album: $e", toastType: ToastType.error); } } @@ -114,24 +122,26 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ]; } - showModalBottomSheet( - backgroundColor: context.colorScheme.surfaceContainer, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), - ), - ); - }, + unawaited( + showModalBottomSheet( + backgroundColor: context.colorScheme.surfaceContainer, + isScrollControlled: false, + context: context, + builder: (context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), + ), + ); + }, + ), ); } - buildOwnerInfo() { + Widget buildOwnerInfo() { if (isOwner) { - final owner = ref.watch(currentUserProvider); + 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)), @@ -139,7 +149,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context), ); } else { - final usersProvider = ref.watch(driftUsersProvider); + final usersProvider = ref.read(driftUsersProvider); return usersProvider.maybeWhen( data: (users) { final user = users.firstWhereOrNull((u) => u.id == album.ownerId); @@ -160,7 +170,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { } } - buildSharedUsersList() { + Widget buildSharedUsersList() { return sharedUsersAsync.maybeWhen( data: (sharedUsers) => ListView.builder( primary: false, @@ -181,7 +191,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ); } - buildSectionTitle(String text) { + Padding buildSectionTitle(String text) { return Padding( padding: const EdgeInsets.all(16.0), child: Text(text, style: context.textTheme.bodySmall), diff --git a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart index a12ca4932b..d256cc2220 100644 --- a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart @@ -7,12 +7,12 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @RoutePage() -class DriftAssetSelectionTimelinePage extends ConsumerWidget { +class DriftAssetSelectionTimelinePage extends StatelessWidget { final Set lockedSelectionAssets; const DriftAssetSelectionTimelinePage({super.key, this.lockedSelectionAssets = const {}}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return ProviderScope( overrides: [ multiSelectProvider.overrideWith( diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 3e5b603451..7e98d25cf0 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -8,13 +10,13 @@ import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @RoutePage() -class AssetTroubleshootPage extends ConsumerWidget { +class AssetTroubleshootPage extends StatelessWidget { final BaseAsset asset; const AssetTroubleshootPage({super.key, required this.asset}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('asset_troubleshoot'.tr())), body: SingleChildScrollView( @@ -27,13 +29,13 @@ class AssetTroubleshootPage extends ConsumerWidget { } } -class _AssetDetailsView extends ConsumerWidget { +class _AssetDetailsView extends StatelessWidget { final BaseAsset asset; const _AssetDetailsView({required this.asset}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -78,11 +80,13 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection @override void initState() { super.initState(); - _buildAssetProperties(widget.asset).whenComplete(() { - if (mounted) { - setState(() {}); - } - }); + unawaited( + _buildAssetProperties(widget.asset).whenComplete(() { + if (mounted) { + setState(() {}); + } + }), + ); } @override @@ -276,8 +280,8 @@ class _RemoteAssetSection extends ConsumerWidget { return const SizedBox.shrink(); } - return FutureBuilder( - future: assetService.getRemoteAssetByChecksum(asset.checksum!), + return FutureBuilder>( + future: assetService.getAllRemoteAssetDebugByChecksum(asset.checksum!), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const _PropertySectionCard( @@ -293,7 +297,7 @@ class _RemoteAssetSection extends ConsumerWidget { ); } - final remoteAsset = snapshot.data; + final remoteAsset = snapshot.data?.firstOrNull; if (remoteAsset == null) { return _PropertySectionCard( diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index 0dfae062dc..93534d62a5 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -186,13 +186,17 @@ class _DriftCreateAlbumPageState extends ConsumerState { assets: selectedAssets, ); - if (album != null && context.mounted) { - unawaited(context.replaceRoute(RemoteAlbumRoute(album: album))); + if (!mounted || album == null) { + return; } + + unawaited(context.replaceRoute(RemoteAlbumRoute(album: album))); } catch (_) { - if (context.mounted) { - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t()); + if (!mounted) { + return; } + + ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t()); } finally { if (mounted) { setState(() => isCreatingAlbum = false); diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index e93a58be7d..740de441e7 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -21,11 +21,11 @@ import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() -class DriftLibraryPage extends ConsumerWidget { +class DriftLibraryPage extends StatelessWidget { const DriftLibraryPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return const Scaffold( body: CustomScrollView( slivers: [ @@ -354,7 +354,7 @@ class _QuickAccessButtonList extends ConsumerWidget { return SliverPadding( padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32), sliver: SliverToBoxAdapter( - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), borderRadius: const BorderRadius.all(Radius.circular(20)), @@ -370,7 +370,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ), child: ListView( shrinkWrap: true, - padding: const EdgeInsets.all(0), + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), children: [ ListTile( @@ -422,7 +422,7 @@ class _PartnerList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( - padding: const EdgeInsets.all(0), + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: partners.length, shrinkWrap: true, diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart index 9849558c94..e8130f7059 100644 --- a/mobile/lib/presentation/pages/drift_locked_folder.page.dart +++ b/mobile/lib/presentation/pages/drift_locked_folder.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -39,8 +41,8 @@ class _DriftLockedFolderPageState extends ConsumerState w return; } if (state == AppLifecycleState.paused) { - ref.read(authProvider.notifier).lockPinCode(); - context.navigateTo(const TabShellRoute()); + unawaited(ref.read(authProvider.notifier).lockPinCode()); + unawaited(context.navigateTo(const TabShellRoute())); return; } setState(() { diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/drift_map.page.dart index 97062b88ab..d36ccc350c 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/drift_map.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -11,8 +13,8 @@ class DriftMapPage extends StatelessWidget { const DriftMapPage({super.key, this.initialLocation}); - void onSettingsPressed(BuildContext context) { - showModalBottomSheet( + Future onSettingsPressed(BuildContext context) { + return showModalBottomSheet( elevation: 0.0, showDragHandle: true, isScrollControlled: true, diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index f601bf8419..31371fe581 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -7,10 +9,10 @@ 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/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/utils/system_ui.utils.dart'; import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; @@ -47,21 +49,21 @@ class DriftMemoryPage extends HookConsumerWidget { useEffect(() { // Memories is an immersive activity - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive)); return () { // Clean up to normal edge to edge when we are done - restoreEdgeToEdge(); + unawaited(restoreEdgeToEdge()); }; }); - toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + void toNextMemory() { + unawaited(memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn)); } void toPreviousMemory() { if (currentMemoryIndex.value > 0) { // Move to the previous memory page - memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + unawaited(memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn)); // Wait for the next frame to ensure the page is built SchedulerBinding.instance.addPostFrameCallback((_) { @@ -83,36 +85,36 @@ class DriftMemoryPage extends HookConsumerWidget { } } - toNextAsset(int currentAssetIndex) { + void toNextAsset(int currentAssetIndex) { if (currentAssetIndex + 1 < currentMemory.value.assets.length) { // Go to the next asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; + final PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); + unawaited(controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500))); } else { // Go to the next memory since we are at the end of our assets toNextMemory(); } } - toPreviousAsset(int currentAssetIndex) { + void toPreviousAsset(int currentAssetIndex) { if (currentAssetIndex > 0) { // Go to the previous asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; + final PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); + unawaited(controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500))); } else { // Go to the previous memory since we are at the end of our assets toPreviousMemory(); } } - updateProgressText() { + void updateProgressText() { assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; } /// Downloads and caches the image for the asset at this [currentMemory]'s index - precacheAsset(int index) async { + Future precacheAsset(int index) async { // Guard index out of range if (index < 0) { return; @@ -153,7 +155,7 @@ class DriftMemoryPage extends HookConsumerWidget { // Precache the next page right away if we are on the first page if (currentAssetPage.value == 0) { - Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); + unawaited(Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1))); } Future onAssetChanged(int otherIndex) async { @@ -198,7 +200,7 @@ class DriftMemoryPage extends HookConsumerWidget { final offset = notification.metrics.pixels; if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { - context.maybePop(); + unawaited(context.maybePop()); return true; } } @@ -281,7 +283,7 @@ class DriftMemoryPage extends HookConsumerWidget { final asset = memories[mIndex].assets[index]; return Stack( children: [ - Container( + ColoredBox( color: Colors.black, child: DriftMemoryCard( asset: asset, @@ -328,8 +330,8 @@ class DriftMemoryPage extends HookConsumerWidget { // auto_route doesn't invoke pop scope, so // turn off full screen mode here // https://github.com/Milad-Akarie/auto_route_library/issues/1799 - context.maybePop(); - restoreEdgeToEdge(); + unawaited(context.maybePop()); + unawaited(restoreEdgeToEdge()); }, shape: const CircleBorder(), color: Colors.white.withValues(alpha: 0.2), diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index 7df96cf78e..70de434d9f 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -56,7 +56,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { _inTimeline = widget.partner.inTimeline; } - _toggleInTimeline() async { + Future _toggleInTimeline() async { final user = ref.read(currentUserProvider); if (user == null) { return; @@ -72,13 +72,16 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { }); } catch (error, stack) { dPrint(() => "Failed to toggle in timeline: $error $stack"); + if (!mounted) { + return; + } + ImmichToast.show( context: context, toastType: ToastType.error, durationInSecond: 1, msg: "Failed to toggle the timeline setting", ); - return; } } @@ -89,7 +92,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { height: 110, child: Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), borderRadius: const BorderRadius.all(Radius.circular(20)), diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index 0afe723dc6..416b7d587a 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -1,11 +1,13 @@ +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/providers/infrastructure/people.provider.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'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/people.utils.dart'; @@ -86,7 +88,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState { late DriftPerson _person; @override - initState() { + void initState() { super.initState(); _person = widget.person; } @@ -49,8 +51,8 @@ class _DriftPersonPageState extends ConsumerState { } } - void showOptionSheet(BuildContext context) { - showModalBottomSheet( + Future showOptionSheet(BuildContext context) { + return showModalBottomSheet( context: context, backgroundColor: context.colorScheme.surface, isScrollControlled: false, diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index ccbddb99f3..f9e59da0a3 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -42,6 +42,9 @@ class _RemoteAlbumPageState extends ConsumerState { Future addAssets(BuildContext context) async { final notifier = ref.read(remoteAlbumProvider.notifier); final albumAssets = await notifier.getAssets(_album.id); + if (!context.mounted) { + return; + } final newAssets = await context.pushRoute>( DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()), @@ -52,8 +55,11 @@ class _RemoteAlbumPageState extends ConsumerState { } final added = await notifier.addAssetsToAlbum(_album.id, newAssets); + if (!context.mounted) { + return; + } - if (added > 0 && context.mounted) { + if (added > 0) { ImmichToast.show( context: context, msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}), @@ -70,23 +76,27 @@ class _RemoteAlbumPageState extends ConsumerState { } try { - await ref.read(remoteAlbumProvider.notifier).addUsers(_album.id, newUsers); - - if (newUsers.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), - toastType: ToastType.success, - ); + if (!context.mounted) { + return; } + await ref.read(remoteAlbumProvider.notifier).addUsers(_album.id, newUsers); ref.invalidate(remoteAlbumSharedUsersProvider(_album.id)); - } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, - msg: "Failed to add users to album: ${e.toString()}", - toastType: ToastType.error, + msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + toastType: ToastType.success, ); + } catch (e) { + if (!context.mounted) { + return; + } + + ImmichToast.show(context: context, msg: "Failed to add users to album: $e", toastType: ToastType.error); } } @@ -127,7 +137,14 @@ class _RemoteAlbumPageState extends ConsumerState { if (confirmed == true) { try { + if (!context.mounted) { + return; + } + await ref.read(remoteAlbumProvider.notifier).deleteAlbum(_album.id); + if (!context.mounted) { + return; + } ImmichToast.show( context: context, @@ -137,6 +154,10 @@ class _RemoteAlbumPageState extends ConsumerState { unawaited(context.pushRoute(const DriftAlbumsRoute())); } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: 'album_viewer_appbar_share_err_delete'.t(context: context), @@ -153,7 +174,11 @@ class _RemoteAlbumPageState extends ConsumerState { builder: (context) => _EditAlbumDialog(album: _album), ); - if (result != null && context.mounted) { + if (!context.mounted) { + return; + } + + if (result != null) { setState(() { _album = _album.copyWith(name: result.name, description: result.description ?? ''); }); @@ -251,20 +276,23 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { await ref .read(remoteAlbumProvider.notifier) .updateAlbum(widget.album.id, name: newTitle, description: newDescription); + if (!mounted) { + return; + } - if (mounted) { - Navigator.of( - context, - ).pop(_EditAlbumData(name: newTitle, description: newDescription.isEmpty ? null : newDescription)); - } + Navigator.of( + context, + ).pop(_EditAlbumData(name: newTitle, description: newDescription.isEmpty ? null : newDescription)); } catch (e) { - if (mounted) { - ImmichToast.show( - context: context, - msg: 'album_update_error'.t(context: context), - toastType: ToastType.error, - ); + if (!mounted) { + return; } + + ImmichToast.show( + context: context, + msg: 'album_update_error'.t(context: context), + toastType: ToastType.error, + ); } } @@ -411,7 +439,7 @@ class _AlbumKebabMenu extends ConsumerWidget { return FutureBuilder( future: ref - .read(remoteAlbumServiceProvider) + .watch(remoteAlbumServiceProvider) .getUserRole(album.id, user?.id ?? '') .then((role) => role == AlbumUserRole.editor), builder: (context, snapshot) { diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 260ed3ba78..e6c78796d3 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -54,7 +54,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si bool _disableAnimations = false; @override - initState() { + void initState() { super.initState(); _config = ref.read(appConfigProvider.select((s) => s.slideshow)); final asset = ref.read(assetViewerProvider).currentAsset; @@ -67,7 +67,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si _updateNextIndex(); ref.listenManual(appConfigProvider.select((s) => s.slideshow), _onConfigChanged); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive)); unawaited(WakelockPlus.enable()); } @@ -78,7 +78,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si } @override - dispose() { + void dispose() { _timer.cancel(); _stopwatch.stop(); _pageController.dispose(); @@ -94,9 +94,9 @@ class _DriftSlideshowPageState extends ConsumerState with Si if (asset.isImage) { _createTimer(); } else if (ref.read(videoPlayerProvider(asset.heroTag)).status == VideoPlaybackStatus.paused) { - ref.read(videoPlayerProvider(asset.heroTag).notifier).play(); + unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).play()); } else { - _nextPage(); + unawaited(_nextPage()); } _updateNextIndex(); @@ -113,7 +113,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si final asset = widget.timeline.getAssetSafe(_index)!; if (!asset.isImage) { - ref.read(videoPlayerProvider(asset.heroTag).notifier).pause(); + unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).pause()); } setState(() { @@ -147,11 +147,11 @@ class _DriftSlideshowPageState extends ConsumerState with Si }; if (!widget.timeline.hasRange(_nextIndex, 1)) { - widget.timeline.preloadAssets(_nextIndex); + unawaited(widget.timeline.preloadAssets(_nextIndex)); } } - void _nextPage() async { + Future _nextPage() async { if (_nextIndex < 0 || _nextIndex >= widget.timeline.totalAssets) { if (_config.repeat) { final wrapped = _config.direction == SlideshowDirection.forward ? 0 : widget.timeline.totalAssets - 1; @@ -184,14 +184,16 @@ class _DriftSlideshowPageState extends ConsumerState with Si _crossfadeFromIndex = previousIndex; _crossfadeToIndex = page; }); - _crossfadeController.forward(from: 0.0).whenComplete(() { - if (mounted) { - setState(() { - _crossfadeFromIndex = null; - _crossfadeToIndex = null; - }); - } - }); + unawaited( + _crossfadeController.forward(from: 0.0).whenComplete(() { + if (mounted) { + setState(() { + _crossfadeFromIndex = null; + _crossfadeToIndex = null; + }); + } + }), + ); } Widget _getCrossfadeLayer(BuildContext context, int index, {required bool isIncoming}) { @@ -238,7 +240,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si _timer = Timer(Duration(milliseconds: _config.duration * 1000 - _stopwatch.elapsedMilliseconds), () { _stopwatch.stop(); _stopwatch.reset(); - _nextPage(); + unawaited(_nextPage()); }); _stopwatch.start(); @@ -267,7 +269,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si _updateNextIndex(); } - void _onTapUp() async { + Future _onTapUp() async { await (_showAppBar ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive) : restoreEdgeToEdge()); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -295,10 +297,10 @@ class _DriftSlideshowPageState extends ConsumerState with Si } else { return LinearProgressIndicator( color: context.colorScheme.primary, - borderRadius: const BorderRadius.all(Radius.zero), + borderRadius: BorderRadius.zero, minHeight: 5, value: - ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds / + ref.read(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds / asset.duration.inMilliseconds, ); } @@ -313,7 +315,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si return ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( image: DecorationImage( image: getFullImageProvider(asset, size: Size(context.width, context.height)), @@ -372,13 +374,13 @@ class _DriftSlideshowPageState extends ConsumerState with Si builder: (context, value, _) => buildPhotoView(scale * (1.0 + value * _kenBurnsZoom)), ); } else { - final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status)); + final status = ref.read(videoPlayerProvider(asset.heroTag).select((s) => s.status)); final position = ref.read(videoPlayerProvider(asset.heroTag)).position; if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) { - _nextPage(); + unawaited(_nextPage()); } else if (status == VideoPlaybackStatus.playing) { - ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false); + unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false)); } return PhotoView.customChild( @@ -418,7 +420,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si IconButton( onPressed: () { _pause(); - context.pushRoute(SettingsSubRoute(section: SettingSection.assetViewer)); + unawaited(context.pushRoute(SettingsSubRoute(section: SettingSection.assetViewer))); }, icon: const Icon(Icons.settings), ), @@ -512,7 +514,7 @@ class _SlideshowProgressBarState extends State<_SlideshowProgressBar> with Singl animationBehavior: AnimationBehavior.preserve, )..value = (widget.elapsedMs / widget.durationMs).clamp(0.0, 1.0); if (!widget.paused) { - _controller.forward(); + unawaited(_controller.forward()); } } @@ -539,7 +541,7 @@ class _SlideshowProgressBarState extends State<_SlideshowProgressBar> with Singl animation: _controller, builder: (context, _) => LinearProgressIndicator( color: widget.color, - borderRadius: const BorderRadius.all(Radius.zero), + borderRadius: BorderRadius.zero, minHeight: 5, value: _controller.value, ), diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart index d21b437efe..36db74a658 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -6,8 +6,8 @@ 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'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -41,7 +41,7 @@ class DriftTrashPage extends StatelessWidget { pinned: true, centerTitle: true, elevation: 0, - actions: [const _TrashKebabMenu()], + actions: const [_TrashKebabMenu()], ), topSliverWidgetHeight: 24, topSliverWidget: Consumer( diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index b73913fd02..a145b96a70 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -52,11 +54,11 @@ class DriftUserSelectionPage extends HookConsumerWidget { final AsyncValue> suggestedShareUsers = ref.watch(driftUsersProvider); final sharedUsersList = useState>({}); - addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); + void addNewUsersHandler() { + unawaited(context.maybePop(sharedUsersList.value.map((e) => e.id).toList())); } - buildTileIcon(UserDto user) { + Widget buildTileIcon(UserDto user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); } else { @@ -64,8 +66,8 @@ class DriftUserSelectionPage extends HookConsumerWidget { } } - buildUserList(List users) { - List usersChip = []; + ListView buildUserList(List users) { + final List usersChip = []; for (var user in sharedUsersList.value) { usersChip.add( @@ -91,7 +93,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { ListView.builder( primary: false, shrinkWrap: true, - itemBuilder: ((context, index) { + itemBuilder: (context, index) { return ListTile( leading: buildTileIcon(users[index]), dense: true, @@ -107,7 +109,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { } }, ); - }), + }, itemCount: users.length, ), ], @@ -122,12 +124,12 @@ class DriftUserSelectionPage extends HookConsumerWidget { leading: IconButton( icon: const Icon(Icons.close_rounded), onPressed: () { - context.maybePop(null); + unawaited(context.maybePop(null)); }, ), actions: [ TextButton( - onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, + onPressed: sharedUsersList.value.isEmpty ? null : () => addNewUsersHandler(), child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), ), ], @@ -135,7 +137,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { body: suggestedShareUsers.widgetWhen( onData: (users) { // Get shared users for this album from the database - final sharedUsers = ref.watch(remoteAlbumSharedUsersProvider(album.id)); + final sharedUsers = ref.read(remoteAlbumSharedUsersProvider(album.id)); return sharedUsers.when( data: (albumSharedUsers) { diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart index 2e2d39e386..0fe5d28850 100644 --- a/mobile/lib/presentation/pages/edit/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -15,7 +15,7 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/editor.utils.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_ui/immich_ui.dart'; -import 'package:openapi/api.dart' show RotateParameters, MirrorParameters, MirrorAxis; +import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; @RoutePage() class DriftEditImagePage extends ConsumerStatefulWidget { @@ -59,9 +59,17 @@ class _DriftEditImagePageState extends ConsumerState with Ti try { await widget.applyEdits(edits); + if (!mounted) { + return; + } + ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); Navigator.of(context).pop(); } catch (e) { + if (!mounted) { + return; + } + ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); } finally { ref.read(editorStateProvider.notifier).setIsEditing(false); @@ -99,7 +107,7 @@ class _DriftEditImagePageState extends ConsumerState with Ti return; } final shouldDiscard = await _showDiscardChangesDialog() ?? false; - if (shouldDiscard && mounted) { + if (shouldDiscard && context.mounted) { Navigator.of(context).pop(); } }, @@ -212,7 +220,7 @@ class _AspectRatioSelector extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final editorState = ref.watch(editorStateProvider); - final editorNotifier = ref.read(editorStateProvider.notifier); + final editorNotifier = ref.watch(editorStateProvider.notifier); return SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -237,7 +245,7 @@ class _TransformControls extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final editorNotifier = ref.read(editorStateProvider.notifier); + final editorNotifier = ref.watch(editorStateProvider.notifier); return Column( mainAxisSize: MainAxisSize.min, @@ -325,7 +333,7 @@ class _ResetEditsButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final editorState = ref.watch(editorStateProvider); - final editorNotifier = ref.read(editorStateProvider.notifier); + final editorNotifier = ref.watch(editorStateProvider.notifier); return ImmichTextButton( labelText: 'reset'.tr(), @@ -376,7 +384,7 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi @override Widget build(BuildContext context) { final editorState = ref.watch(editorStateProvider); - final editorNotifier = ref.read(editorStateProvider.notifier); + final editorNotifier = ref.watch(editorStateProvider.notifier); ref.listen(editorStateProvider, (previous, current) { // Only re-apply the aspect ratio when it changes, otherwise the crop rect will shrink on every rotation diff --git a/mobile/lib/presentation/pages/edit/editor.provider.dart b/mobile/lib/presentation/pages/edit/editor.provider.dart index 69c8b7bf54..39cf4cc3ff 100644 --- a/mobile/lib/presentation/pages/edit/editor.provider.dart +++ b/mobile/lib/presentation/pages/edit/editor.provider.dart @@ -1,10 +1,14 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; 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/domain/models/exif.model.dart'; import 'package:immich_mobile/utils/editor.utils.dart'; +part 'editor.provider.freezed.dart'; + final editorStateProvider = NotifierProvider(EditorProvider.new); class EditorProvider extends Notifier { @@ -25,15 +29,15 @@ class EditorProvider extends Notifier { final originalWidth = exifInfo.isFlipped ? exifInfo.height : exifInfo.width; final originalHeight = exifInfo.isFlipped ? exifInfo.width : exifInfo.height; - Rect crop = existingCrop != null && originalWidth != null && originalHeight != null + final Rect crop = existingCrop != null && originalWidth != null && originalHeight != null ? convertCropParametersToRect(existingCrop.parameters, originalWidth, originalHeight) : const Rect.fromLTRB(0, 0, 1, 1); final transform = normalizeTransformEdits(edits); state = state.copyWith( - originalWidth: originalWidth, - originalHeight: originalHeight, + originalWidth: originalWidth ?? state.originalWidth, + originalHeight: originalHeight ?? state.originalHeight, crop: crop, flipHorizontal: transform.mirrorHorizontal, flipVertical: transform.mirrorVertical, @@ -106,104 +110,24 @@ class EditorProvider extends Notifier { } } -class EditorState { - final bool isApplyingEdits; +@freezed +abstract class EditorState with _$EditorState { + const EditorState._(); - final int rotationAngle; - final bool flipHorizontal; - final bool flipVertical; - final Rect crop; - final CropAspectRatio aspectRatio; - - final int originalWidth; - final int originalHeight; - - final Duration animationDuration; - - final bool hasUnsavedEdits; - - const EditorState({ - bool? isApplyingEdits, - int? rotationAngle, - bool? flipHorizontal, - bool? flipVertical, - Rect? crop, - CropAspectRatio? aspectRatio, - int? originalWidth, - int? originalHeight, - Duration? animationDuration, - bool? hasUnsavedEdits, - }) : isApplyingEdits = isApplyingEdits ?? false, - rotationAngle = rotationAngle ?? 0, - flipHorizontal = flipHorizontal ?? false, - flipVertical = flipVertical ?? false, - animationDuration = animationDuration ?? Duration.zero, - originalWidth = originalWidth ?? 0, - originalHeight = originalHeight ?? 0, - crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1), - aspectRatio = aspectRatio ?? CropAspectRatio.free, - hasUnsavedEdits = hasUnsavedEdits ?? false; - - EditorState copyWith({ - bool? isApplyingEdits, - int? rotationAngle, - bool? flipHorizontal, - bool? flipVertical, - CropAspectRatio? aspectRatio, - int? originalWidth, - int? originalHeight, - Duration? animationDuration, - Rect? crop, - bool? hasUnsavedEdits, - }) { - return EditorState( - isApplyingEdits: isApplyingEdits ?? this.isApplyingEdits, - rotationAngle: rotationAngle ?? this.rotationAngle, - flipHorizontal: flipHorizontal ?? this.flipHorizontal, - flipVertical: flipVertical ?? this.flipVertical, - aspectRatio: aspectRatio ?? this.aspectRatio, - animationDuration: animationDuration ?? this.animationDuration, - originalWidth: originalWidth ?? this.originalWidth, - originalHeight: originalHeight ?? this.originalHeight, - crop: crop ?? this.crop, - hasUnsavedEdits: hasUnsavedEdits ?? this.hasUnsavedEdits, - ); - } + const factory EditorState({ + @Default(false) bool isApplyingEdits, + @Default(0) int rotationAngle, + @Default(false) bool flipHorizontal, + @Default(false) bool flipVertical, + @Default(Rect.fromLTRB(0, 0, 1, 1)) Rect crop, + @Default(CropAspectRatio.free) CropAspectRatio aspectRatio, + @Default(0) int originalWidth, + @Default(0) int originalHeight, + @Default(Duration.zero) Duration animationDuration, + @Default(false) bool hasUnsavedEdits, + }) = _EditorState; bool get hasEdits { return rotationAngle != 0 || flipHorizontal || flipVertical || crop != const Rect.fromLTRB(0, 0, 1, 1); } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is EditorState && - other.isApplyingEdits == isApplyingEdits && - other.rotationAngle == rotationAngle && - other.flipHorizontal == flipHorizontal && - other.flipVertical == flipVertical && - other.crop == crop && - other.aspectRatio == aspectRatio && - other.originalWidth == originalWidth && - other.originalHeight == originalHeight && - other.animationDuration == animationDuration && - other.hasUnsavedEdits == hasUnsavedEdits; - } - - @override - int get hashCode { - return isApplyingEdits.hashCode ^ - rotationAngle.hashCode ^ - flipHorizontal.hashCode ^ - flipVertical.hashCode ^ - crop.hashCode ^ - aspectRatio.hashCode ^ - originalWidth.hashCode ^ - originalHeight.hashCode ^ - animationDuration.hashCode ^ - hasUnsavedEdits.hashCode; - } } 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 3fb32b7d93..523781b274 100644 --- a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -70,11 +70,15 @@ class _ProfilePictureCropPageState extends ConsumerState final croppedImage = await _cropController.croppedImage(); final pngBytes = await imageToUint8List(croppedImage); final xFile = XFile.fromData(pngBytes, mimeType: 'image/png'); + if (!context.mounted) { + return; + } + final success = await ref .read(uploadProfileImageProvider.notifier) .upload(xFile, fileName: 'profile-picture.png'); - if (!context.mounted) { + if (!mounted) { return; } @@ -94,9 +98,7 @@ class _ProfilePictureCropPageState extends ConsumerState toastType: ToastType.success, ); - if (context.mounted) { - unawaited(context.maybePop()); - } + unawaited(context.maybePop()); } else { ImmichToast.show( context: context, @@ -106,7 +108,7 @@ class _ProfilePictureCropPageState extends ConsumerState ); } } catch (e) { - if (!context.mounted) { + if (!mounted) { return; } @@ -157,7 +159,7 @@ class _ProfilePictureCropPageState extends ConsumerState return Center( child: ConstrainedBox( constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(7)), boxShadow: [ diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 22546b8e50..12935b4f23 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -80,7 +80,7 @@ class DriftSearchPage extends HookConsumerWidget { final userPreferences = ref.watch(userMetadataPreferencesProvider); - search(SearchFilter f) { + void search(SearchFilter f) { if (f == filter.value) { return; } @@ -94,7 +94,7 @@ class DriftSearchPage extends HookConsumerWidget { } } - loadMoreSearchResults() { + void loadMoreSearchResults() { unawaited(ref.read(paginatedSearchProvider.notifier).search(filter.value)); } @@ -105,106 +105,116 @@ class DriftSearchPage extends HookConsumerWidget { return null; } - Future.microtask(() { - textSearchController.clear(); - peopleCurrentFilterWidget.value = null; - dateRangeCurrentFilterWidget.value = null; - cameraCurrentFilterWidget.value = null; - tagCurrentFilterWidget.value = null; - mediaTypeCurrentFilterWidget.value = null; - ratingCurrentFilterWidget.value = null; - displayOptionCurrentFilterWidget.value = null; - locationCurrentFilterWidget.value = preFilter.location.city != null - ? Text(preFilter.location.city!, style: context.textTheme.labelLarge) - : null; - search(preFilter); - }); + unawaited( + Future.microtask(() { + if (!context.mounted) { + return; + } + + textSearchController.clear(); + peopleCurrentFilterWidget.value = null; + dateRangeCurrentFilterWidget.value = null; + cameraCurrentFilterWidget.value = null; + tagCurrentFilterWidget.value = null; + mediaTypeCurrentFilterWidget.value = null; + ratingCurrentFilterWidget.value = null; + displayOptionCurrentFilterWidget.value = null; + locationCurrentFilterWidget.value = preFilter.location.city != null + ? Text(preFilter.location.city!, style: context.textTheme.labelLarge) + : null; + search(preFilter); + }), + ); return null; }, [preFilter]); - showPeoplePicker() { + void showPeoplePicker() { var people = filter.value.people; - handleOnSelect(Set value) { + void handleOnSelect(Set value) { people = value; } - handleClear() { + void handleClear() { peopleCurrentFilterWidget.value = null; search(filter.value.copyWith(people: {})); } - handleApply() { + void handleApply() { final label = people.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '); peopleCurrentFilterWidget.value = label.isNotEmpty ? Text(label, style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(people: people)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.t(context: context), - expanded: true, - onSearch: handleApply, - onClear: handleClear, - child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'search_filter_people_title'.t(context: context), + expanded: true, + onSearch: handleApply, + onClear: handleClear, + child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), + ), ), ), ); } - showTagPicker() { + void showTagPicker() { var tagIds = filter.value.tagIds ?? []; String tagLabel = ''; - handleOnSelect(Iterable tags) { + void handleOnSelect(Iterable tags) { tagIds = tags.map((t) => t.id).toList(); tagLabel = tags.map((t) => t.value).join(', '); } - handleClear() { + void handleClear() { tagCurrentFilterWidget.value = null; search(filter.value.copyWith(tagIds: [])); } - handleApply() { + void handleApply() { tagCurrentFilterWidget.value = tagLabel.isNotEmpty ? Text(tagLabel, style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(tagIds: tagIds)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_tags_title'.t(context: context), - expanded: true, - onSearch: handleApply, - onClear: handleClear, - child: TagPicker(onSelectExistingTag: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'search_filter_tags_title'.t(context: context), + expanded: true, + onSearch: handleApply, + onClear: handleClear, + child: TagPicker(onSelectExistingTag: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()), + ), ), ), ); } - showLocationPicker() { + void showLocationPicker() { var location = filter.value.location; - handleOnSelect(Map value) { + void handleOnSelect(Map value) { location = SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']); } - handleClear() { + void handleClear() { locationCurrentFilterWidget.value = null; search(filter.value.copyWith(location: SearchLocationFilter())); } - handleApply() { + void handleApply() { final locationText = [ if (location.country != null) location.country!, if (location.state != null) location.state!, @@ -216,21 +226,23 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(location: location)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Container( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + child: FilterBottomSheetScaffold( + title: 'search_filter_location_title'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Container( + padding: EdgeInsets.only(bottom: context.viewInsets.bottom), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), + ), ), ), ), @@ -238,19 +250,19 @@ class DriftSearchPage extends HookConsumerWidget { ); } - showCameraPicker() { + void showCameraPicker() { var camera = filter.value.camera; - handleOnSelect(Map value) { + void handleOnSelect(Map value) { camera = SearchCameraFilter(make: value['make'], model: value['model']); } - handleClear() { + void handleClear() { cameraCurrentFilterWidget.value = null; search(filter.value.copyWith(camera: SearchCameraFilter())); } - handleApply() { + void handleApply() { final make = camera.make ?? ''; final model = camera.model ?? ''; cameraCurrentFilterWidget.value = (make.isNotEmpty || model.isNotEmpty) @@ -259,23 +271,25 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(camera: camera)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + child: FilterBottomSheetScaffold( + title: 'search_filter_camera_title'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), + ), ), ), ); } - datePicked(DateFilterInputModel? selectedDate) { + void datePicked(DateFilterInputModel? selectedDate) { dateInputFilter.value = selectedDate; if (selectedDate == null) { dateRangeCurrentFilterWidget.value = null; @@ -298,7 +312,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - showDatePicker() async { + Future showDatePicker() async { final firstDate = DateTime(1900); final lastDate = DateTime.now(); @@ -338,42 +352,44 @@ class DriftSearchPage extends HookConsumerWidget { } } - showQuickDatePicker() { - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: "pick_date_range".tr(), - expanded: true, - onClear: () => datePicked(null), - child: QuickDatePicker( - currentInput: dateInputFilter.value, - onRequestPicker: () { - ContextHelper(context).pop(); - showDatePicker(); - }, - onSelect: (date) { - ContextHelper(context).pop(); - datePicked(date); - }, + void showQuickDatePicker() { + unawaited( + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: "pick_date_range".tr(), + expanded: true, + onClear: () => datePicked(null), + child: QuickDatePicker( + currentInput: dateInputFilter.value, + onRequestPicker: () { + ContextHelper(context).pop(); + unawaited(showDatePicker()); + }, + onSelect: (date) { + ContextHelper(context).pop(); + datePicked(date); + }, + ), ), ), ); } // MEDIA PICKER - showMediaTypePicker() { + void showMediaTypePicker() { var mediaType = filter.value.mediaType; - handleOnSelected(AssetType assetType) { + void handleOnSelected(AssetType assetType) { mediaType = assetType; } - handleClear() { + void handleClear() { mediaTypeCurrentFilterWidget.value = null; search(filter.value.copyWith(mediaType: AssetType.other)); } - handleApply() { + void handleApply() { mediaTypeCurrentFilterWidget.value = mediaType != AssetType.other ? Text( mediaType == AssetType.image ? 'image'.t(context: context) : 'video'.t(context: context), @@ -383,31 +399,33 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(mediaType: mediaType)); } - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), + unawaited( + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'search_filter_media_type_title'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), + ), ), ); } // STAR RATING PICKER - showStarRatingPicker() { + void showStarRatingPicker() { var rating = filter.value.rating; - handleOnSelected(SearchRatingFilter value) { + void handleOnSelected(SearchRatingFilter value) { rating = value; } - handleClear() { + void handleClear() { ratingCurrentFilterWidget.value = null; search(filter.value.copyWith(rating: SearchRatingFilter())); } - handleApply() { + void handleApply() { ratingCurrentFilterWidget.value = rating.rating.isSome ? Text( 'rating_count'.t(args: {'count': rating.rating.unwrapOrNull ?? 0}), @@ -417,23 +435,25 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(rating: rating)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FilterBottomSheetScaffold( - title: 'rating'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FilterBottomSheetScaffold( + title: 'rating'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), + ), ), ); } // DISPLAY OPTION - showDisplayOptionPicker() { + void showDisplayOptionPicker() { var display = filter.value.display; - handleOnSelect(Map value) { + void handleOnSelect(Map value) { display = display.copyWith( isNotInAlbum: value[DisplayOption.notInAlbum], isArchive: value[DisplayOption.archive], @@ -441,7 +461,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - handleClear() { + void handleClear() { displayOptionCurrentFilterWidget.value = null; search( filter.value.copyWith( @@ -450,7 +470,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - handleApply() { + void handleApply() { final filterText = [ if (display.isNotInAlbum) 'search_filter_display_option_not_in_album'.t(context: context), if (display.isArchive) 'archive'.t(context: context), @@ -462,18 +482,20 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(display: display)); } - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'display_options'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), + unawaited( + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'display_options'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), + ), ), ); } - handleTextSubmitted(String value) => search(switch (textSearchType.value) { + void handleTextSubmitted(String value) => search(switch (textSearchType.value) { TextSearchType.context => filter.value.copyWith(filename: '', context: value, description: '', ocr: ''), TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''), TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''), @@ -598,7 +620,7 @@ class DriftSearchPage extends HookConsumerWidget { ), ), ], - title: Container( + title: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), borderRadius: const BorderRadius.all(Radius.circular(24)), @@ -697,7 +719,7 @@ class DriftSearchPage extends HookConsumerWidget { if (filter.value.isEmpty) const _SearchSuggestions() else - _SearchResultGrid(onScrollEnd: loadMoreSearchResults), + _SearchResultGrid(onScrollEnd: () => loadMoreSearchResults()), ], ), ); @@ -727,7 +749,7 @@ class _SearchResultGrid extends ConsumerWidget { } Widget? _bottomWidget(BuildContext context, WidgetRef ref) { - final isLoading = ref.watch(paginatedSearchProvider.select((s) => s.isLoading)); + final isLoading = ref.read(paginatedSearchProvider.select((s) => s.isLoading)); if (isLoading) { return const SliverFillRemaining( @@ -739,7 +761,7 @@ class _SearchResultGrid extends ConsumerWidget { ); } - final hasMore = ref.watch(paginatedSearchProvider.select((s) => s.nextPage != null)); + final hasMore = ref.read(paginatedSearchProvider.select((s) => s.nextPage != null)); if (hasMore) { return null; @@ -859,7 +881,7 @@ class _QuickLinkList extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(20)), border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1), diff --git a/mobile/lib/presentation/pages/search/paginated_search.provider.dart b/mobile/lib/presentation/pages/search/paginated_search.provider.dart index fa2d5a06cf..e9839f31ea 100644 --- a/mobile/lib/presentation/pages/search/paginated_search.provider.dart +++ b/mobile/lib/presentation/pages/search/paginated_search.provider.dart @@ -70,7 +70,7 @@ class PaginatedSearchNotifier extends StateNotifier { @override void dispose() { - _assetCountController.close(); + unawaited(_assetCountController.close()); super.dispose(); } } 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 dc48ed57ec..cfb5887c7c 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,27 +1,27 @@ +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/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/providers/user.provider.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/constants/enums.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.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'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_ui/immich_ui.dart'; -enum AddToMenuItem { album, archive, unarchive, lockedFolder } +enum AddToMenuItem { album } class AddActionButton extends ConsumerStatefulWidget { const AddActionButton({super.key, this.originalTheme}); @@ -37,16 +37,6 @@ class _AddActionButtonState extends ConsumerState { switch (selected) { case AddToMenuItem.album: _openAlbumSelector(); - break; - case AddToMenuItem.archive: - performArchiveAction(context, ref, source: ActionSource.viewer); - break; - case AddToMenuItem.unarchive: - performUnArchiveAction(context, ref, source: ActionSource.viewer); - break; - case AddToMenuItem.lockedFolder: - performMoveToLockFolderAction(context, ref, source: ActionSource.viewer); - break; } } @@ -58,11 +48,6 @@ class _AddActionButtonState extends ConsumerState { final user = ref.read(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; - final isInLockedView = ref.watch(inLockedViewProvider); - final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; - final hasRemote = asset is RemoteAsset; - final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived; - final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived; return [ Padding( @@ -82,26 +67,8 @@ class _AddActionButtonState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text("move_to".tr(), style: context.textTheme.labelMedium), ), - if (showArchive) - BaseActionButton( - iconData: Icons.archive_outlined, - label: "archive".tr(), - menuItem: true, - onPressed: () => _handleMenuSelection(AddToMenuItem.archive), - ), - if (showUnarchive) - BaseActionButton( - iconData: Icons.unarchive_outlined, - label: "unarchive".tr(), - menuItem: true, - onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive), - ), - BaseActionButton( - iconData: Icons.lock_outline, - label: "locked_folder".tr(), - menuItem: true, - onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder), - ), + const ActionMenuItem(action: ArchiveAction(source: .viewer)), + const ActionMenuItem(action: LockAction(source: .viewer)), ], ]; } @@ -118,21 +85,23 @@ class _AddActionButtonState extends ConsumerState { AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album)), ]; - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) { - return BaseBottomSheet( - actions: const [], - slivers: slivers, - initialChildSize: 0.6, - minChildSize: 0.3, - maxChildSize: 0.95, - expand: false, - backgroundColor: context.isDarkTheme ? Colors.black : Colors.white, - ); - }, + unawaited( + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) { + return BaseBottomSheet( + actions: const [], + slivers: slivers, + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 0.95, + expand: false, + backgroundColor: context.isDarkTheme ? Colors.black : Colors.white, + ); + }, + ), ); } @@ -146,7 +115,7 @@ class _AddActionButtonState extends ConsumerState { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album); - if (!context.mounted) { + if (!mounted) { return; } @@ -177,7 +146,7 @@ class _AddActionButtonState extends ConsumerState { ); } - if (!context.mounted) { + if (!mounted) { return; } await Navigator.of(context).maybePop(); @@ -192,7 +161,7 @@ class _AddActionButtonState extends ConsumerState { final themeData = widget.originalTheme ?? context.themeData; - return MenuAnchor( + return ImmichMenu( consumeOutsideTap: true, style: MenuStyle( backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor), @@ -203,7 +172,7 @@ class _AddActionButtonState extends ConsumerState { ), padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)), ), - menuChildren: widget.originalTheme != null + children: widget.originalTheme != null ? [ Theme( data: widget.originalTheme!, diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart deleted file mode 100644 index bb2cae21ad..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -// used to allow performing archive action from different sources (without duplicating code) -Future performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).archive(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } -} - -class ArchiveActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const ArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - await performArchiveAction(context, ref, source: source); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.archive_outlined, - label: "to_archive".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart index ee2dace729..bb25fc4739 100644 --- a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -class BaseActionButton extends ConsumerWidget { +class BaseActionButton extends StatelessWidget { const BaseActionButton({ super.key, required this.label, @@ -31,7 +30,7 @@ class BaseActionButton extends ConsumerWidget { final void Function()? onLongPressed; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0); final iconTheme = IconTheme.of(context); final iconSize = iconTheme.size ?? 24.0; diff --git a/mobile/lib/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart deleted file mode 100644 index b9ac47cd57..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class BulkTagAssetsActionButton extends ConsumerWidget { - final ActionSource source; - - const BulkTagAssetsActionButton({super.key, required this.source}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - final result = await ref.read(actionProvider.notifier).tagAssets(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - if (!context.mounted) { - return; - } - - ImmichToast.show( - context: context, - msg: result.success - ? 'tagged_assets'.t(context: context, args: {'count': result.count.toString()}) - : 'errors.failed_to_tag_assets'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.sell_outlined, - label: "control_bottom_app_bar_add_tags".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart deleted file mode 100644 index 7a4f84fb4f..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; - -class CastActionButton extends ConsumerWidget { - const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - - final bool iconOnly; - final bool menuItem; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - return BaseActionButton( - iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - iconColor: isCasting ? context.primaryColor : null, // null = default color - label: "cast".t(context: context), - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - iconOnly: iconOnly, - menuItem: menuItem, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart deleted file mode 100644 index 2a7c4ba9d7..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Set the deletedAt information, put the asset in the trash in the server -/// which will be permanently deleted after the number of days configure by the admin -/// - Prompt to delete the asset locally -class DeleteActionButton extends ConsumerWidget { - final ActionSource source; - final bool showConfirmation; - final bool iconOnly; - final bool menuItem; - const DeleteActionButton({ - super.key, - required this.source, - this.showConfirmation = false, - this.iconOnly = false, - this.menuItem = false, - }); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - if (showConfirmation) { - final confirm = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('delete'.t(context: context)), - content: Text('delete_action_confirmation_message'.t(context: context)), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text('cancel'.t(context: context)), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text( - 'confirm'.t(context: context), - style: TextStyle(color: context.colorScheme.error), - ), - ), - ], - ), - ); - if (confirm != true) { - return; - } - } - - final currentAsset = ref.read(assetViewerProvider).currentAsset; - final stackIndex = ref.read(assetViewerProvider).stackIndex; - - final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - - if (source == ActionSource.viewer && result.success) { - final shouldRefreshStack = currentAsset is RemoteAsset && currentAsset.stackId != null; - EventStream.shared.emit( - shouldRefreshStack ? ViewerStackAssetDeletedEvent(stackIndex: stackIndex) : const ViewerReloadAssetEvent(), - ); - } - - final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 110.0, - iconData: Icons.delete_sweep_outlined, - label: "delete".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart deleted file mode 100644 index 6911d09f89..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; - -/// This delete action has the following behavior: -/// - Prompt to delete the asset locally -class DeleteLocalActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const DeleteLocalActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).deleteLocal(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - if (result.count == 0) { - return; - } - - ref.invalidate(localAlbumProvider); - - final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 95.0, - iconData: Icons.no_cell_outlined, - label: "control_bottom_app_bar_delete_from_local".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart deleted file mode 100644 index 267a9f55e6..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Delete permanently on the server -/// - Prompt to delete the asset locally -class DeletePermanentActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - final bool useShortLabel; - - const DeletePermanentActionButton({ - super.key, - required this.source, - this.iconOnly = false, - this.menuItem = false, - this.useShortLabel = false, - }); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final count = source == ActionSource.viewer ? 1 : ref.read(multiSelectProvider).selectedAssets.length; - final confirm = - await showDialog( - context: context, - builder: (context) => PermanentDeleteDialog(count: count), - ) ?? - false; - if (!confirm) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'delete_permanently_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 110.0, - iconData: Icons.delete_forever, - label: useShortLabel ? "delete".t(context: context) : "delete_permanently".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart deleted file mode 100644 index d19a188561..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Delete permanently on the server -/// - Prompt to delete the asset locally -/// -/// This action is used when the asset is selected in multi-selection mode in the trash page -class DeleteTrashActionButton extends ConsumerWidget { - final ActionSource source; - - const DeleteTrashActionButton({super.key, required this.source}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length)); - - final confirmDelete = - await showDialog( - context: context, - builder: (context) => PermanentDeleteDialog(count: selectCount), - ) ?? - false; - if (!confirmDelete) { - return; - } - - final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'assets_permanently_deleted_count'.t( - context: context, - args: {'count': result.count.toString()}, - ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextButton.icon( - icon: Icon(Icons.delete_forever, color: Colors.red[400]), - label: Text( - "delete".t(context: context), - style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold), - ), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart deleted file mode 100644 index a5129b643a..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:immich_mobile/constants/enums.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/utils/background_sync.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; - -class DownloadActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { - if (!context.mounted) { - return; - } - - try { - await ref.read(actionProvider.notifier).downloadAll(source); - - Future.delayed(const Duration(seconds: 1), () async { - await backgroundSyncManager.syncLocal(); - await backgroundSyncManager.hashAssets(); - }); - } finally { - ref.read(multiSelectProvider.notifier).reset(); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final backgroundManager = ref.watch(backgroundSyncProvider); - - return BaseActionButton( - iconData: Icons.download, - maxWidth: 95, - label: "download".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref, backgroundManager), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart index efa7f5c6d0..264e489db3 100644 --- a/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,7 +35,7 @@ class DownloadStatusFloatingButton extends ConsumerWidget { : context.colorScheme.surfaceBright, elevation: 2, onPressed: () { - context.pushRoute(const DownloadInfoRoute()); + unawaited(context.pushRoute(const DownloadInfoRoute())); }, child: Stack( alignment: AlignmentDirectional.center, diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart deleted file mode 100644 index 6eeec0658b..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class EditDateTimeActionButton extends ConsumerWidget { - final ActionSource source; - - const EditDateTimeActionButton({super.key, required this.source}); - - _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).editDateTime(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'edit_date_and_time_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 95.0, - iconData: Icons.edit_calendar_outlined, - label: "control_bottom_app_bar_edit_time".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart deleted file mode 100644 index 564b02d884..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ /dev/null @@ -1,59 +0,0 @@ -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/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset_edit.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class EditImageActionButton extends ConsumerWidget { - const EditImageActionButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); - - Future editImage(List edits) async { - if (currentAsset == null || currentAsset.remoteId == null) { - return; - } - - await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits); - } - - Future onPress() async { - if (currentAsset == null || currentAsset.remoteId == null) { - return; - } - - final imageProvider = getFullImageProvider(currentAsset, edited: false); - - final image = Image(image: imageProvider); - final (edits, exifInfo) = await ( - ref.read(remoteAssetRepositoryProvider).getAssetEdits(currentAsset.remoteId!), - ref.read(remoteAssetRepositoryProvider).getExif(currentAsset.remoteId!), - ).wait; - - if (exifInfo == null) { - return; - } - - ref.read(editorStateProvider.notifier).init(edits, exifInfo); - await context.pushRoute(DriftEditImageRoute(image: image, applyEdits: editImage)); - } - - return BaseActionButton( - iconData: Icons.tune, - label: "edit".t(context: context), - onPressed: onPress, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart deleted file mode 100644 index 1a8a1a5c39..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class EditLocationActionButton extends ConsumerWidget { - final ActionSource source; - - const EditLocationActionButton({super.key, required this.source}); - - _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).editLocation(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'edit_location_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.edit_location_alt_outlined, - label: "control_bottom_app_bar_edit_location".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart deleted file mode 100644 index 07ace7e631..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class FavoriteActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).favorite(source); - - if (source == ActionSource.viewer) { - if (result.success) { - final currentAsset = ref.read(assetViewerProvider).currentAsset; - if (currentAsset is RemoteAsset && !currentAsset.isFavorite) { - ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: true)); - } - } - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'favorite_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.favorite_border_rounded, - label: "favorite".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} 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 4cb973cca1..46a5c2c41e 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 @@ -25,7 +25,7 @@ class LikeActivityActionButton extends ConsumerWidget { final activities = ref.watch(albumActivityProvider((album?.id ?? "", asset?.id))); - onTap(Activity? liked) async { + Future onTap(Activity? liked) async { if (user == null) { return; } 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 3bd67978e2..878c589b21 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 @@ -17,7 +17,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), - onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle, + onPressed: ref.watch(isPlayingMotionVideoProvider.notifier).toggle, iconOnly: iconOnly, menuItem: menuItem, ); diff --git a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart deleted file mode 100644 index 56191e9055..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -// Reusable helper: move to locked folder from any source (e.g called from menu) -Future performMoveToLockFolderAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'move_to_lock_folder_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } -} - -class MoveToLockFolderActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const MoveToLockFolderActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - await performMoveToLockFolderAction(context, ref, source: source); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 115.0, - iconData: Icons.lock_outline_rounded, - label: "move_to_locked_folder".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart deleted file mode 100644 index 541a9f8093..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class OpenInBrowserActionButton extends ConsumerWidget { - final String remoteId; - final TimelineOrigin origin; - final bool iconOnly; - final bool menuItem; - - const OpenInBrowserActionButton({ - super.key, - required this.remoteId, - required this.origin, - this.iconOnly = false, - this.menuItem = false, - }); - - void _onTap() async { - final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', ''); - - String originPath = ''; - switch (origin) { - case TimelineOrigin.favorite: - originPath = '/favorites'; - break; - case TimelineOrigin.trash: - originPath = '/trash'; - break; - case TimelineOrigin.archive: - originPath = '/archive'; - break; - default: - break; - } - - final url = '$serverEndpoint$originPath/photos/$remoteId'; - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - label: 'open_in_browser'.t(context: context), - iconData: Icons.open_in_browser, - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: _onTap, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart deleted file mode 100644 index 97a36a56dc..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RemoveFromAlbumActionButton extends ConsumerWidget { - final String albumId; - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const RemoveFromAlbumActionButton({ - super.key, - required this.albumId, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'remove_from_album_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.remove_circle_outline, - label: "remove_from_album".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart deleted file mode 100644 index 17d2a76af7..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RemoveFromLockFolderActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const RemoveFromLockFolderActionButton({ - super.key, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).removeFromLockFolder(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'remove_from_lock_folder_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 100.0, - iconData: Icons.lock_open_rounded, - label: "remove_from_locked_folder".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart deleted file mode 100644 index 1713718967..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RestoreActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const RestoreActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).restoreTrash(source); - ref.read(multiSelectProvider.notifier).reset(); - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.history_rounded, - label: 'restore'.t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100.0, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart deleted file mode 100644 index e7928bd325..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RestoreTrashActionButton extends ConsumerWidget { - final ActionSource source; - - const RestoreTrashActionButton({super.key, required this.source}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).restoreTrash(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextButton.icon( - icon: const Icon(Icons.history_rounded), - label: Text('restore'.t(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart deleted file mode 100644 index 1d704aafe8..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class SetAlbumCoverActionButton extends ConsumerWidget { - final String albumId; - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const SetAlbumCoverActionButton({ - super.key, - required this.albumId, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'album_cover_updated'.t(context: context); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.image_outlined, - label: 'set_as_album_cover'.t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart deleted file mode 100644 index c8dbb7cb1f..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SetProfilePictureActionButton extends ConsumerWidget { - final BaseAsset asset; - final bool iconOnly; - final bool menuItem; - - const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context) { - if (!context.mounted) { - return; - } - - context.pushRoute(ProfilePictureCropRoute(asset: asset)); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.account_circle_outlined, - label: "set_as_profile_picture".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart deleted file mode 100644 index eef87f299d..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -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/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class _SharePreparingDialog extends StatelessWidget { - final ValueNotifier progress; - - const _SharePreparingDialog({required this.progress}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container(margin: const EdgeInsets.only(bottom: 12), child: const Text('share_dialog_preparing').tr()), - SizedBox( - width: 240, - child: ValueListenableBuilder( - valueListenable: progress, - builder: (context, value, _) { - final percent = value == null ? null : (value * 100).clamp(0, 100); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - LinearProgressIndicator(value: value, minHeight: 8.0), - if (percent != null) - Container(margin: const EdgeInsets.only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')), - ], - ); - }, - ), - ), - ], - ), - ); - } -} - -class _ShareFileTypeDialog extends StatelessWidget { - final bool showPreview; - - const _ShareFileTypeDialog({this.showPreview = true}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(context.t.select_quality), - contentPadding: const EdgeInsets.symmetric(vertical: 8), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.high_quality_rounded), - title: Text(context.t.share_original), - onTap: () => context.pop(ShareAssetType.original), - ), - if (showPreview) - ListTile( - leading: const Icon(Icons.photo_size_select_large_rounded), - title: Text(context.t.share_preview), - onTap: () => context.pop(ShareAssetType.preview), - ), - ], - ), - actions: [TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel))], - ); - } -} - -class ShareActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const ShareActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Set _getSelectedAssets(WidgetRef ref) { - return switch (source) { - ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, - ActionSource.viewer => switch (ref.read(assetViewerProvider).currentAsset) { - BaseAsset asset => {asset}, - null => const {}, - }, - }; - } - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final fileType = ref.read(appConfigProvider).share.fileType; - await _share(context, ref, fileType); - } - - void _onLongPress(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - // only show preview option when at least one of the assets is not a video - // we cant share previews of videos - final assets = _getSelectedAssets(ref); - final showPreview = assets.isEmpty || assets.any((asset) => !asset.isVideo); - - final fileType = await showDialog( - context: context, - builder: (_) => _ShareFileTypeDialog(showPreview: showPreview), - useRootNavigator: false, - ); - - if (fileType == null || !context.mounted) { - return; - } - - await _share(context, ref, fileType); - } - - Future _share(BuildContext context, WidgetRef ref, ShareAssetType fileType) async { - final cancelCompleter = Completer(); - final progress = ValueNotifier(null); - final preparingDialog = _SharePreparingDialog(progress: progress); - await showDialog( - context: context, - builder: (BuildContext buildContext) { - ref - .read(actionProvider.notifier) - .shareAssets( - source, - context, - fileType: fileType, - cancelCompleter: cancelCompleter, - onAssetDownloadProgress: (value) => progress.value = value, - ) - .then((ActionResult result) { - if (cancelCompleter.isCompleted || !context.mounted) { - return; - } - - if (!result.success) { - ImmichToast.show( - context: context, - msg: context.t.scaffold_body_error_occurred, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - - buildContext.pop(); - }); - - return preparingDialog; - }, - barrierDismissible: false, - useRootNavigator: false, - ).then((_) { - if (!cancelCompleter.isCompleted) { - cancelCompleter.complete(); - } - progress.dispose(); - }); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: context.t.share, - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - onLongPressed: () => _onLongPress(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart deleted file mode 100644 index b8dc69f515..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; - -class ShareLinkActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const ShareLinkActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - await ref.read(actionProvider.notifier).shareLink(source, context); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.link_rounded, - label: "share_link".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart deleted file mode 100644 index 42dcfa683a..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ /dev/null @@ -1,58 +0,0 @@ -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/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SimilarPhotosActionButton extends ConsumerWidget { - final String assetId; - final bool iconOnly; - final bool menuItem; - - const SimilarPhotosActionButton({super.key, required this.assetId, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - ref.invalidate(assetViewerProvider); - ref.invalidate(paginatedSearchProvider); - - ref.read(searchPreFilterProvider.notifier) - ..clear() - ..setFilter( - SearchFilter( - assetId: assetId, - people: {}, - location: SearchLocationFilter(), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ); - - unawaited(context.navigateTo(const DriftSearchRoute())); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.compare, - label: "view_similar_photos".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart deleted file mode 100644 index 479cf2dfe9..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SlideshowActionButton extends ConsumerWidget { - final bool iconOnly; - final bool menuItem; - - const SlideshowActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) { - if (!context.mounted) { - return; - } - - context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.slideshow, - label: "slideshow".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart deleted file mode 100644 index 22fccf5473..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class StackActionButton extends ConsumerWidget { - final ActionSource source; - - const StackActionButton({super.key, required this.source}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final user = ref.watch(currentUserProvider); - if (user == null) { - throw Exception('User must be logged in to access stack action'); - } - - final result = await ref.read(actionProvider.notifier).stack(user.id, source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'stack_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.filter_none_rounded, - label: "stack".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart deleted file mode 100644 index e95569af45..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Set the deletedAt information, put the asset in the trash in the server -/// which will be permanently deleted after the number of days configure by the admin -class TrashActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const TrashActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).trash(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'trash_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 85.0, - iconData: Icons.delete_outline_rounded, - label: "control_bottom_app_bar_trash_from_immich".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart deleted file mode 100644 index 57221303a8..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ /dev/null @@ -1,61 +0,0 @@ -// dart -// File: `lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart` -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; - -// used to allow performing unarchive action from different sources (without duplicating code) -Future performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).unArchive(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } -} - -class UnArchiveActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UnArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - await performUnArchiveAction(context, ref, source: source); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.unarchive_outlined, - label: "unarchive".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart deleted file mode 100644 index 5e88735d9c..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class UnFavoriteActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).unFavorite(source); - - if (source == ActionSource.viewer) { - if (result.success) { - final currentAsset = ref.read(assetViewerProvider).currentAsset; - if (currentAsset is RemoteAsset && currentAsset.isFavorite) { - ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: false)); - } - } - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'unfavorite_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.favorite_rounded, - label: "unfavorite".t(context: context), - onPressed: () => _onTap(context, ref), - iconOnly: iconOnly, - menuItem: menuItem, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart deleted file mode 100644 index e7badf129f..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class UnStackActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UnStackActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).unStack(source); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'unstack_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.layers_clear_outlined, - label: "unstack".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart deleted file mode 100644 index 599e11d467..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/services/view_intent.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_ui/immich_ui.dart'; - -class UploadActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UploadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final isTimeline = source == ActionSource.timeline; - final viewerIntentFilePath = source == ActionSource.viewer ? ref.read(viewIntentFilePathProvider) : null; - List? assets; - var isUploadDialogOpen = false; - var wasUploadCancelled = false; - Future? uploadDialogFuture; - - if (source == ActionSource.timeline) { - assets = ref.read(multiSelectProvider).selectedAssets.whereType().toList(); - if (assets.isEmpty) { - return; - } - ref.read(multiSelectProvider.notifier).reset(); - } else { - isUploadDialogOpen = true; - uploadDialogFuture = - showDialog( - context: context, - barrierDismissible: false, - builder: (dialogContext) => _UploadProgressDialog( - onCancel: () { - wasUploadCancelled = true; - }, - ), - ).whenComplete(() { - isUploadDialogOpen = false; - }); - unawaited(uploadDialogFuture); - } - - var success = false; - if (!isTimeline && viewerIntentFilePath != null) { - final viewIntentService = ref.read(viewIntentServiceProvider); - viewIntentService.markUploadActive(viewerIntentFilePath); - var hasError = false; - try { - await ref - .read(foregroundUploadServiceProvider) - .uploadShareIntent( - [File(viewerIntentFilePath)], - onError: (_, _) { - hasError = true; - }, - ); - } finally { - await viewIntentService.markUploadInactive(viewerIntentFilePath); - } - success = !hasError; - } else { - final result = await ref.read(actionProvider.notifier).upload(source, assets: assets); - success = result.success; - } - - if (!isTimeline && context.mounted && isUploadDialogOpen) { - Navigator.of(context, rootNavigator: true).pop(); - } - - if (context.mounted && !success && !wasUploadCancelled) { - ImmichToast.show( - context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.backup_outlined, - label: "upload".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} - -class _UploadProgressDialog extends ConsumerWidget { - final VoidCallback onCancel; - - const _UploadProgressDialog({required this.onCancel}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final progressMap = ref.watch(assetUploadProgressProvider); - - // Calculate overall progress from all assets - final values = progressMap.values.where((v) => v >= 0).toList(); - final progress = values.isEmpty ? 0.0 : values.reduce((a, b) => a + b) / values.length; - final hasError = progressMap.values.any((v) => v < 0); - final percentage = (progress * 100).toInt(); - - return AlertDialog( - title: Text('uploading'.t(context: context)), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (hasError) - const Icon(Icons.error_outline, color: Colors.red, size: 48) - else - CircularProgressIndicator(value: progress > 0 ? progress : null), - const SizedBox(height: 16), - Text(hasError ? 'Error' : '$percentage%'), - ], - ), - actions: [ - ImmichTextButton( - onPressed: () { - ref.read(manualUploadCancelTokenProvider)?.complete(); - ref.read(manualUploadCancelTokenProvider.notifier).state = null; - onCancel(); - Navigator.of(context, rootNavigator: true).pop(); - }, - labelText: 'cancel'.t(context: context), - ), - ], - ); - } -} diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 44abfba47e..9babdb5b57 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -63,7 +63,7 @@ class _AlbumSelectorState extends ConsumerState { isGrid = albumConfig.isGrid; }); - ref.read(remoteAlbumProvider.notifier).refresh(); + unawaited(ref.read(remoteAlbumProvider.notifier).refresh()); }); searchController.addListener(() { @@ -81,7 +81,7 @@ class _AlbumSelectorState extends ConsumerState { final userId = ref.read(currentUserProvider)?.id; filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode); - filterAlbums(); + unawaited(filterAlbums()); } Future onRefresh() async { @@ -92,7 +92,7 @@ class _AlbumSelectorState extends ConsumerState { setState(() { isGrid = !isGrid; }); - ref.read(settingsProvider).write(.albumIsGrid, isGrid); + unawaited(ref.read(settingsProvider).write(.albumIsGrid, isGrid)); } void changeFilter(QuickFilterMode mode) { @@ -100,7 +100,7 @@ class _AlbumSelectorState extends ConsumerState { filter = filter.copyWith(mode: mode); }); - filterAlbums(); + unawaited(filterAlbums()); } Future changeSort(AlbumSort sort) async { @@ -121,7 +121,7 @@ class _AlbumSelectorState extends ConsumerState { searchController.clear(); }); - filterAlbums(); + unawaited(filterAlbums()); } Future sortAlbums() async { @@ -395,7 +395,7 @@ class _SearchBar extends StatelessWidget { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), sliver: SliverToBoxAdapter( - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), borderRadius: const BorderRadius.all(Radius.circular(24)), @@ -669,7 +669,7 @@ class _GridAlbumCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); + final albumThumbnailAsset = ref.watch(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); return GestureDetector( onTap: () => onAlbumSelected(album), @@ -699,7 +699,7 @@ class _GridAlbumCard extends ConsumerWidget { ); } - return Container( + return ColoredBox( color: context.colorScheme.surfaceContainerHighest, child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey), ); @@ -746,7 +746,7 @@ class AddToAlbumHeader extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { Future onCreateAlbum() async { final albumName = await showDialog(context: context, builder: (context) => const NewAlbumNameModal()); - if (albumName == null) { + if (albumName == null || !context.mounted) { return; } @@ -755,6 +755,10 @@ class AddToAlbumHeader extends ConsumerWidget { .read(remoteAlbumProvider.notifier) .createAlbumWithAssets(title: albumName, assets: selectedAssets); + if (!context.mounted) { + return; + } + if (newAlbum == null) { ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); return; @@ -774,7 +778,7 @@ class AddToAlbumHeader extends ConsumerWidget { TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // remove internal padding - minimumSize: const Size(0, 0), // allow shrinking + minimumSize: Size.zero, // allow shrinking tapTargetSize: MaterialTapTargetSize.shrinkWrap, // remove extra height ), onPressed: onCreateAlbum, @@ -797,8 +801,8 @@ class CreateAlbumButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Future onCreateAlbum() async { - var albumName = await showDialog(context: context, builder: (context) => const NewAlbumNameModal()); - if (albumName == null) { + final albumName = await showDialog(context: context, builder: (context) => const NewAlbumNameModal()); + if (albumName == null || !context.mounted) { return; } @@ -813,6 +817,10 @@ class CreateAlbumButton extends ConsumerWidget { .read(remoteAlbumProvider.notifier) .createAlbum(title: albumName, assetIds: [asset.remoteId!]); + if (!context.mounted) { + return; + } + if (album == null) { ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); return; @@ -839,7 +847,7 @@ class CreateAlbumButton extends ConsumerWidget { TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - minimumSize: const Size(0, 0), + minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: onCreateAlbum, diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart index 1aeadf61bc..598666cda9 100644 --- a/mobile/lib/presentation/widgets/album/album_tile.dart +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -17,7 +17,7 @@ class AlbumTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); + final albumThumbnailAsset = ref.watch(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); return LargeLeadingTile( title: Text( @@ -51,7 +51,7 @@ class AlbumTile extends ConsumerWidget { : SizedBox( width: 80, height: 80, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(16)), 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 2701316e75..5622ed6b1b 100644 --- a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart +++ b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -45,8 +47,8 @@ class PendingUploadsBanner extends ConsumerWidget { ); } - static void _openSheet(BuildContext context, String albumId) { - showModalBottomSheet( + static Future _openSheet(BuildContext context, String albumId) { + return showModalBottomSheet( context: context, showDragHandle: true, builder: (_) => _PendingUploadsSheet(albumId: albumId), @@ -97,7 +99,7 @@ class _PendingUploadsBannerContent extends StatelessWidget { return Material( color: hasFailures ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainerHigh, child: InkWell( - onTap: () => PendingUploadsBanner._openSheet(context, albumId), + onTap: () => unawaited(PendingUploadsBanner._openSheet(context, albumId)), child: Column( mainAxisSize: MainAxisSize.min, children: [ 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 6a565fa2cd..dffda47b84 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 @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -8,8 +9,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; 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 27bac68310..eff5705eae 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 @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -8,6 +9,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/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'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -27,16 +29,15 @@ class DateTimeDetails extends ConsumerWidget { final asset = this.asset; final exifInfo = this.exifInfo; final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null); + final editDateTime = const EditDateTimeAction(source: .viewer).create(context, ref); return Column( children: [ SheetTile( title: _getDateTime(context, asset, exifInfo), titleStyle: context.textTheme.labelLarge, - trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, - onTap: asset.hasRemote && isOwner - ? () async => await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context) - : null, + trailing: editDateTime == null ? null : const Icon(Icons.edit, size: 18), + onTap: editDateTime?.onAction, ), if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner), ], @@ -93,6 +94,9 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> if (!editAction.success) { _controller.text = previousDescription ?? ''; + if (!mounted) { + return; + } ImmichToast.show( context: context, 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 8c144a83bd..c6d048e458 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 @@ -1,13 +1,14 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.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/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -48,15 +49,11 @@ class _LocationDetailsState extends ConsumerState { if (widget.exifInfo != oldWidget.exifInfo) { final exif = widget.exifInfo; if (exif != null && exif.hasCoordinates) { - _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exif.latitude!, exif.longitude!))); + unawaited(_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exif.latitude!, exif.longitude!)))); } } } - void editLocation() async { - await ref.read(actionProvider.notifier).editLocation(ActionSource.viewer, context); - } - @override Widget build(BuildContext context) { final asset = widget.asset; @@ -68,6 +65,7 @@ class _LocationDetailsState extends ConsumerState { return const SizedBox.shrink(); } + final editLocation = const EditLocationAction(source: .viewer).create(context, ref); final locationName = _getLocationName(exifInfo); final coordinates = "${exifInfo?.latitude?.toStringAsFixed(4)}, ${exifInfo?.longitude?.toStringAsFixed(4)}"; @@ -79,8 +77,8 @@ class _LocationDetailsState extends ConsumerState { SheetTile( title: 'location'.t(context: context), titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null, - onTap: editLocation, + trailing: hasCoordinates && editLocation != null ? const Icon(Icons.edit_location_alt, size: 20) : null, + onTap: editLocation?.onAction, ), if (hasCoordinates) Padding( @@ -115,7 +113,7 @@ class _LocationDetailsState extends ConsumerState { color: context.primaryColor, ), leading: const Icon(Icons.location_off), - onTap: editLocation, + onTap: editLocation?.onAction, ), ], ), 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 278da294c0..72db236b3c 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 @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -74,7 +76,7 @@ class PeopleDetails extends ConsumerWidget { return; } ContextHelper(context).pop(); - context.pushRoute(DriftPersonRoute(person: person)); + unawaited(context.pushRoute(DriftPersonRoute(person: person))); }, onNameTap: () => showNameEditModal(person), ), 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 e501c2ee3e..352838c761 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 @@ -41,7 +41,7 @@ class RatingDetails extends ConsumerWidget { unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100), itemSize: 40, onRatingUpdate: (rating) async { - await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round()); + await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating); }, onClearRating: () async { await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, null); 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 33e0fa38f5..e92b671562 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 @@ -72,7 +72,7 @@ class TechnicalDetails extends ConsumerWidget { final subtitleStyle = context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary); if (asset is LocalAsset) { - final assetMediaRepository = ref.watch(assetMediaRepositoryProvider); + final assetMediaRepository = ref.read(assetMediaRepositoryProvider); return FutureBuilder( future: assetMediaRepository.getOriginalFilename(asset.id), builder: (context, snapshot) { @@ -99,14 +99,14 @@ class TechnicalDetails extends ConsumerWidget { static String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) { final height = asset.height; final width = asset.width; - final resolution = (width != null && height != null) ? "${width.toInt()} x ${height.toInt()}" : null; + final resolution = (width != null && height != null) ? "$width x $height" : null; final fileSize = exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null; return switch ((fileSize, resolution)) { (null, null) => '', - (String fileSize, null) => fileSize, - (null, String resolution) => resolution, - (String fileSize, String resolution) => '$fileSize$_kSeparator$resolution', + (final String fileSize, null) => fileSize, + (null, final String resolution) => resolution, + (final String fileSize, final String resolution) => '$fileSize$_kSeparator$resolution', }; } @@ -116,9 +116,9 @@ class TechnicalDetails extends ConsumerWidget { } return switch ((exifInfo.make, exifInfo.model)) { (null, null) => null, - (String make, null) => make, - (null, String model) => model, - (String make, String model) => '$make $model', + (final String make, null) => make, + (null, final String model) => model, + (final String make, final String model) => '$make $model', }; } 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 1ef22a891f..c887f8e65a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -87,8 +87,8 @@ class _AssetPageState extends ConsumerState { @override void dispose() { _scrollController.dispose(); - _scaleBoundarySub?.cancel(); - _eventSubscription?.cancel(); + unawaited(_scaleBoundarySub?.cancel()); + unawaited(_eventSubscription?.cancel()); super.dispose(); } @@ -112,7 +112,7 @@ class _AssetPageState extends ConsumerState { return; } _viewer.setShowingDetails(true); - _scrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic); + unawaited(_scrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic)); } bool _willClose(double scrollVelocity) => @@ -199,7 +199,7 @@ class _AssetPageState extends ConsumerState { case _DragIntent.dismiss: const popThreshold = 75.0; if (details.localPosition.dy - start!.localPosition.dy > popThreshold) { - context.maybePop(); + unawaited(context.maybePop()); return; } _viewController?.animateMultiple( @@ -292,14 +292,14 @@ class _AssetPageState extends ConsumerState { } void _listenForScaleBoundaries(PhotoViewControllerBase? controller) { - _scaleBoundarySub?.cancel(); + unawaited(_scaleBoundarySub?.cancel()); _scaleBoundarySub = null; if (controller == null || controller.scaleBoundaries != null) { return; } _scaleBoundarySub = controller.outputStateStream.listen((_) { if (controller.scaleBoundaries != null) { - _scaleBoundarySub?.cancel(); + unawaited(_scaleBoundarySub?.cancel()); _scaleBoundarySub = null; if (mounted) { setState(() {}); @@ -401,7 +401,7 @@ class _AssetPageState extends ConsumerState { _showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex)); final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); - final timelineOrigin = ref.read(timelineServiceProvider).origin; + final timelineOrigin = ref.watch(timelineServiceProvider).origin; final showingOcr = ref.watch(assetViewerProvider.select((s) => s.showingOcr)); final asset = _asset; diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart index f5d75a6a86..77832c2685 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; class AssetStackRow extends ConsumerWidget { @@ -17,13 +17,19 @@ class AssetStackRow extends ConsumerWidget { return const SizedBox.shrink(); } - final hideAssetStack = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash; + final hideAssetStack = ref.watch(timelineServiceProvider).origin == TimelineOrigin.trash; if (hideAssetStack) { return const SizedBox.shrink(); } + final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); + if (showingDetails) { + return const SizedBox.shrink(); + } + final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); + final double opacity = + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); return IgnorePointer( ignoring: opacity < 1.0, @@ -75,7 +81,7 @@ class _StackItemState extends ConsumerState<_StackItem> { Icons.play_circle_outline_rounded, color: Colors.white, size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], + shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset.zero)], ), ); const selectedDecoration = BoxDecoration( 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 1065ebe25e..23cc6119fb 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -107,7 +107,7 @@ class _AssetViewerState extends ConsumerState { final maxPage = _totalAssets - 1; if (target >= 0 && target <= maxPage) { _pageController.jumpToPage(target); - _onAssetChanged(target); + unawaited(_onAssetChanged(target)); } } @@ -126,14 +126,14 @@ class _AssetViewerState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback(_onAssetInit); final assetViewer = ref.read(assetViewerProvider); - _setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails); + unawaited(_setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails)); } @override void dispose() { _pageController.dispose(); _preloader.dispose(); - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); _stackChildrenKeepAlive?.close(); unawaited(restoreEdgeToEdge()); @@ -157,7 +157,7 @@ class _AssetViewerState extends ConsumerState { final page = _pageController.page?.round(); if (page != null && page != _currentPage) { - _onAssetChanged(page); + unawaited(_onAssetChanged(page)); } return false; } @@ -167,7 +167,7 @@ class _AssetViewerState extends ConsumerState { _handleCasting(); } - void _onAssetChanged(int index) async { + Future _onAssetChanged(int index) async { _currentPage = index; final asset = await ref.read(timelineServiceProvider).getAssetAsync(index); @@ -222,8 +222,8 @@ class _AssetViewerState extends ConsumerState { _onTimelineReloadEvent(); case ViewerReloadAssetEvent(): _onViewerReloadEvent(); - case ViewerStackAssetDeletedEvent event: - _onViewerStackAssetDeletedEvent(event); + case final ViewerStackAssetDeletedEvent event: + unawaited(_onViewerStackAssetDeletedEvent(event)); default: } } @@ -235,8 +235,8 @@ class _AssetViewerState extends ConsumerState { final index = _pageController.page?.round() ?? 0; final target = index >= _totalAssets - 1 ? index - 1 : index + 1; - _pageController.animateToPage(target, duration: Durations.medium1, curve: Curves.easeInOut); - _onAssetChanged(target); + unawaited(_pageController.animateToPage(target, duration: Durations.medium1, curve: Curves.easeInOut)); + unawaited(_onAssetChanged(target)); } Future _onViewerStackAssetDeletedEvent(ViewerStackAssetDeletedEvent event) async { @@ -271,7 +271,7 @@ class _AssetViewerState extends ConsumerState { final totalAssets = timelineService.totalAssets; if (totalAssets == 0) { - context.maybePop(); + unawaited(context.maybePop()); return; } @@ -281,14 +281,14 @@ class _AssetViewerState extends ConsumerState { if (index != _currentPage) { _pageController.jumpToPage(index); - _onAssetChanged(index); + unawaited(_onAssetChanged(index)); } else if (currentAsset is RemoteAsset && currentAsset.stackId != null && assetIndex == null) { final timelineAsset = timelineService.getAssetSafe(index); if (timelineAsset is! RemoteAsset || currentAsset.stackId != timelineAsset.stackId) { - _onAssetChanged(index); + unawaited(_onAssetChanged(index)); } } else if (currentAsset != null && assetIndex == null) { - _onAssetChanged(index); + unawaited(_onAssetChanged(index)); } if (_totalAssets != totalAssets) { @@ -298,9 +298,9 @@ class _AssetViewerState extends ConsumerState { } } - void _setSystemUIMode(bool controls, bool details) { + Future _setSystemUIMode(bool controls, bool details) { final immersive = !controls || (CurrentPlatform.isIOS && details); - unawaited(immersive ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) : restoreEdgeToEdge()); + return immersive ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) : restoreEdgeToEdge(); } @override @@ -324,7 +324,7 @@ class _AssetViewerState extends ConsumerState { ref.listen(assetViewerProvider.select((value) => (value.showingControls, value.showingDetails)), (_, state) { final (controls, details) = state; - _setSystemUIMode(controls, details); + unawaited(_setSystemUIMode(controls, details)); }); return AnnotatedRegion( diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 53a7aee700..4ff839881a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -1,26 +1,31 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_asset.action.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; +import 'package:immich_ui/immich_ui.dart'; + +// Resolved here rather than as an ActionColumnButton so a hidden restore +// takes no slot in the spaceEvenly row below. +List _actionColumnButtons(BuildContext context, WidgetRef ref, List actions) => actions + .map((a) => a.create(context, ref)) + .nonNulls + .map( + (item) => .new(icon: item.icon, label: item.label, onPressed: item.onAction, onLongPress: item.onSecondaryAction), + ) + .toList(growable: false); class ViewerBottomBar extends ConsumerWidget { const ViewerBottomBar({super.key}); @@ -33,37 +38,24 @@ class ViewerBottomBar extends ConsumerWidget { } final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); - final user = ref.watch(currentUserProvider); - final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final isInLockedView = ref.watch(inLockedViewProvider); - final serverInfo = ref.watch(serverInfoProvider); - final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash; + final isInTrash = ref.watch(timelineServiceProvider).origin == TimelineOrigin.trash; final originalTheme = context.themeData; final actions = [ - if (isInTrash && isOwner && asset.hasRemote) - const RestoreActionButton(source: ActionSource.viewer) - else - const ShareActionButton(source: ActionSource.viewer), + ..._actionColumnButtons(context, ref, const [RestoreAction(source: .viewer), ShareAction(source: .viewer)]), if (!isInLockedView) ...[ if (!isInTrash) ...[ - if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - // edit sync was added in 2.6.0 - if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) - const EditImageActionButton(), - if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), - ], - if (isOwner) ...[ - if (asset.isLocalOnly) - const DeleteLocalActionButton(source: ActionSource.viewer) - else if (asset.isTrashed) - const DeletePermanentActionButton(source: ActionSource.viewer, useShortLabel: true) - else - const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true), + ..._actionColumnButtons(context, ref, const [ + UploadAction(source: .viewer, showProgress: true), + EditAssetAction(source: .viewer), + ]), + if (asset.hasRemote) ImmichColorOverride(color: null, child: AddActionButton(originalTheme: originalTheme)), ], + ..._actionColumnButtons(context, ref, const [DeleteAction(source: .viewer)]), ], ]; @@ -104,7 +96,10 @@ class ViewerBottomBar extends ConsumerWidget { if (asset.isImage) OcrToggleButton(asset: asset), if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), if (!isReadonlyModeEnabled) - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + ImmichColorOverride( + color: Colors.white, + child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + ), ], ), ), 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 800af23039..08d6b59423 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 @@ -30,7 +30,7 @@ class MotionPhotoPlayButton extends ConsumerWidget { child: Center( child: _MotionButton( isPlaying: isPlaying, - onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle, + onPressed: ref.watch(isPlayingMotionVideoProvider.notifier).toggle, ), ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart index a9291f3173..576d4937b6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart @@ -82,7 +82,7 @@ class _OcrOverlayState extends ConsumerState { } void _detachController() { - _controllerSub?.cancel(); + unawaited(_controllerSub?.cancel()); _controllerSub = null; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart index 73c7b2d3e7..cdff727967 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart @@ -28,7 +28,7 @@ class OcrToggleButton extends ConsumerWidget { shape: const CircleBorder(), clipBehavior: Clip.antiAlias, child: InkWell( - onTap: ref.read(assetViewerProvider.notifier).toggleOcr, + onTap: ref.watch(assetViewerProvider.notifier).toggleOcr, child: const Padding( padding: EdgeInsets.all(10.0), child: Icon(Icons.text_fields_rounded, size: 22, color: Colors.white), 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 b956ef103c..0ed37869be 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart @@ -64,8 +64,8 @@ class _RatingBarState extends State { } else if (dx >= totalWidth) { newRating = widget.itemCount.toDouble(); } else { - double starWithPadding = widget.itemSize + widget.starPadding; - int tappedIndex = (dx / starWithPadding).floor().clamp(0, widget.itemCount - 1); + final double starWithPadding = widget.itemSize + widget.starPadding; + final int tappedIndex = (dx / starWithPadding).floor().clamp(0, widget.itemCount - 1); newRating = tappedIndex + 1.0; if (isTap && newRating == _currentRating && _currentRating != 0) { @@ -88,7 +88,7 @@ class _RatingBarState extends State { @override Widget build(BuildContext context) { final isRTL = Directionality.of(context) == TextDirection.rtl; - final double visualAlignmentOffset = 5.0; + const double visualAlignmentOffset = 5.0; return Column( mainAxisSize: MainAxisSize.min, @@ -107,8 +107,8 @@ class _RatingBarState extends State { if (i.isOdd) { return SizedBox(width: widget.starPadding); } - int index = i ~/ 2; - bool filled = _currentRating > index; + final int index = i ~/ 2; + final bool filled = _currentRating > index; return widget.itemBuilder ?? Icon( Icons.star_rounded, 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 69e84ee03d..ac6f79a155 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -26,7 +28,7 @@ class SheetTile extends ConsumerWidget { }); void copyTitle(BuildContext context, WidgetRef ref) { - Clipboard.setData(ClipboardData(text: title)); + unawaited(Clipboard.setData(ClipboardData(text: title))); ImmichToast.show( context: context, msg: 'copied_to_clipboard'.t(context: context), diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index ccbaf7660a..8b3d8a9978 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -66,7 +66,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg if (!widget.isCurrent) { _loadTimer?.cancel(); - _notifier.pause(); + unawaited(_notifier.pause()); return; } @@ -83,7 +83,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg } @override - void didChangeAppLifecycleState(AppLifecycleState state) async { + Future didChangeAppLifecycleState(AppLifecycleState state) async { switch (state) { case AppLifecycleState.resumed: if (_shouldPlayOnForeground) { @@ -112,6 +112,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg final localFilePath = widget.localFilePath; if (localFilePath != null) { final file = File(localFilePath); + // ignore: avoid_slow_async_io if (!await file.exists()) { throw Exception('No file found for the video'); } @@ -149,6 +150,10 @@ class _NativeVideoViewerState extends ConsumerState with Widg final remoteAsset = videoAsset as RemoteAsset; final serverEndpoint = Store.get(StoreKey.serverEndpoint); + if (!context.mounted) { + return null; + } + final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo; final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; final String assetId = remoteAsset.livePhotoVideoId ?? remoteAsset.id; @@ -198,7 +203,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg return localAsset; } - void _onPlaybackReady() async { + Future _onPlaybackReady() async { if (!mounted || !widget.isCurrent) { return; } @@ -257,7 +262,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg _controller?.onPlaybackEnded.removeListener(_onPlaybackEnded); } - void _loadVideo() async { + Future _loadVideo() async { final nc = _controller; if (nc == null || nc.videoSource != null || !mounted) { return; @@ -292,7 +297,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg _controller = nc; if (widget.isCurrent) { - _loadVideo(); + unawaited(_loadVideo()); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart index 1c0b600843..0e7b5661de 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; class ViewerBottomAppBar extends ConsumerWidget { const ViewerBottomAppBar({super.key}); @@ -9,7 +9,8 @@ class ViewerBottomAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); + final double opacity = + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); return IgnorePointer( ignoring: opacity < 1.0, diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart index da0abad1dd..cbbc851108 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart @@ -29,7 +29,7 @@ class ViewerKebabMenu extends ConsumerWidget { final user = ref.watch(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - final timelineOrigin = ref.read(timelineServiceProvider).origin; + final timelineOrigin = ref.watch(timelineServiceProvider).origin; final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final isInLockedView = ref.watch(inLockedViewProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider); diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 5997e15bf0..deae9cbd07 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -40,10 +42,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { } final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); + final double opacity = + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); final originalTheme = context.themeData; - final assetForAction = [asset]; final actions = [ if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true), @@ -51,17 +53,19 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { IconButton( icon: const Icon(Icons.chat_outlined), onPressed: () { - context.router.push( - DriftActivitiesRoute( - album: album, - assetId: asset is RemoteAsset ? asset.id : null, - assetName: asset.name, + unawaited( + context.router.push( + DriftActivitiesRoute( + album: album, + assetId: asset is RemoteAsset ? asset.id : null, + assetName: asset.name, + ), ), ); }, ), - ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)), + const ActionIconButton(action: FavoriteAction(source: .viewer)), ImmichColorOverride(color: null, child: ViewerKebabMenu(originalTheme: originalTheme)), ]; 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 3c9c0c692e..3e5edaa149 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 @@ -4,25 +4,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class ArchiveBottomSheet extends ConsumerStatefulWidget { @@ -49,9 +43,6 @@ class _ArchiveBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { - final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); - Future addToAlbum(RemoteAlbum album) async { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); @@ -76,31 +67,23 @@ class _ArchiveBottomSheetState extends ConsumerState { return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [FavoriteAction(assets: assets)]; - return BaseBottomSheet( controller: sheetController, initialChildSize: 0.25, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ - const ShareActionButton(source: ActionSource.timeline), - if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), - const UnArchiveActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), - if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), - ], - if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), + actions: const [ + .new(action: ShareAction(source: .timeline)), + .new(action: ShareLinkAction(source: .timeline)), + .new(action: ArchiveAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart index 0d0e0259fd..a066e0167e 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -53,10 +55,12 @@ class _BaseDraggableScrollableSheetState extends ConsumerState } if (previous?.isInteracting != true && next.isInteracting) { - _controller.animateTo( - widget.minChildSize, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, + unawaited( + _controller.animateTo( + widget.minChildSize, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), ); } }); @@ -74,7 +78,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState color: widget.backgroundColor ?? context.colorScheme.surfaceContainer, elevation: 3.0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18))), - margin: const EdgeInsets.symmetric(horizontal: 0), + margin: EdgeInsets.zero, child: Column( children: [ Expanded( 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 bcb9fc6fe3..7540da4fff 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 @@ -1,28 +1,22 @@ 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/translate_extensions.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/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -32,7 +26,6 @@ class FavoriteBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); Future addAssetsToAlbum(RemoteAlbum album) async { final selectedAssets = multiselect.selectedAssets; @@ -44,6 +37,9 @@ class FavoriteBottomSheet extends ConsumerWidget { final result = await ref .read(remoteAlbumProvider.notifier) .addAssets(album.id, remoteAssets.map((e) => e.id).toList()); + if (!context.mounted) { + return; + } if (selectedAssets.length != remoteAssets.length) { ImmichToast.show( @@ -74,30 +70,22 @@ class FavoriteBottomSheet extends ConsumerWidget { ref.read(multiSelectProvider.notifier).reset(); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [FavoriteAction(assets: assets)]; - return BaseBottomSheet( initialChildSize: 0.4, maxChildSize: 0.7, shouldCloseOnMinExtent: false, - actions: [ - const ShareActionButton(source: ActionSource.timeline), - if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), - const ArchiveActionButton(source: ActionSource.timeline), - if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), - ], - if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), + actions: const [ + .new(action: ShareAction(source: .timeline)), + .new(action: ShareLinkAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), + .new(action: ArchiveAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), ], slivers: multiselect.hasRemote ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)] 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 1949a79495..d5407e768f 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 @@ -4,30 +4,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; +import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; +import 'package:immich_mobile/presentation/actions/tag.action.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class GeneralBottomSheet extends ConsumerStatefulWidget { @@ -54,12 +46,6 @@ class _GeneralBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { - final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); - final tagsEnabled = ref.watch( - userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), - ); - Future addToAlbum(RemoteAlbum album) async { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); @@ -83,37 +69,27 @@ class _GeneralBottomSheetState extends ConsumerState { return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [AssetDebugAction(assets: assets)]; - return BaseBottomSheet( controller: sheetController, initialChildSize: widget.minChildSize ?? 0.15, minChildSize: widget.minChildSize, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), - const ShareActionButton(source: ActionSource.timeline), - if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), - if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), - const FavoriteActionButton(source: ActionSource.timeline), - const ArchiveActionButton(source: ActionSource.timeline), - if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), - if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline), - ], - if (multiselect.onlyLocal || multiselect.hasMerged) - const DeleteLocalActionButton(source: ActionSource.timeline), - if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline), + actions: const [ + .new(action: AssetDebugAction(source: .timeline)), + .new(action: ShareAction(source: .timeline)), + .new(action: ShareLinkAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), + .new(action: ArchiveAction(source: .timeline)), + .new(action: TagAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), + .new(action: UploadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), 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 ac8c77af03..478b37212e 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 @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -64,10 +65,11 @@ class _LocalAlbumBottomSheetState extends ConsumerState { initialChildSize: 0.25, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: const [ - ShareActionButton(source: ActionSource.timeline), - DeleteLocalActionButton(source: ActionSource.timeline), - UploadActionButton(source: ActionSource.timeline), + actions: const [ + .new(action: ShareAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), + .new(action: UploadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index a644e6a035..3064ced2a4 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -1,26 +1,25 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -class LockedFolderBottomSheet extends ConsumerWidget { +class LockedFolderBottomSheet extends StatelessWidget { const LockedFolderBottomSheet({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return const BaseBottomSheet( initialChildSize: 0.25, maxChildSize: 0.4, shouldCloseOnMinExtent: false, - actions: [ - ShareActionButton(source: ActionSource.timeline), - DownloadActionButton(source: ActionSource.timeline), - DeletePermanentActionButton(source: ActionSource.timeline), - RemoveFromLockFolderActionButton(source: ActionSource.timeline), + actions: [ + .new(action: ShareAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), ], ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart index 945bdc9584..06d1ddc99d 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart @@ -21,10 +21,10 @@ class MapBottomSheet extends StatelessWidget { maxChildSize: 0.75, shouldCloseOnMinExtent: false, resizeOnScroll: false, - actions: [], + actions: const [], backgroundColor: context.themeData.colorScheme.surface, - slivers: [ - const SliverFillRemaining(hasScrollBody: false, child: SizedBox(height: 0, child: _ScopedMapTimeline())), + slivers: const [ + SliverFillRemaining(hasScrollBody: false, child: SizedBox(height: 0, child: _ScopedMapTimeline())), ], ); } 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 c49c805e3d..8a20f26c73 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 @@ -3,8 +3,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -63,9 +64,9 @@ class _PartnerDetailBottomSheetState extends ConsumerState[ + .new(action: ShareAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index a292c1899c..1e51a26811 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -4,27 +4,21 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.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/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -53,8 +47,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState @override Widget build(BuildContext context) { - final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId; Future addToAlbum(RemoteAlbum album) async { @@ -85,40 +77,37 @@ class _RemoteAlbumBottomSheetState extends ConsumerState return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [FavoriteAction(assets: assets)]; - return BaseBottomSheet( controller: sheetController, initialChildSize: 0.22, minChildSize: 0.22, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ - const ShareActionButton(source: ActionSource.timeline), - if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), + actions: [ + const .new(action: ShareAction(source: .timeline)), + const .new(action: ShareLinkAction(source: .timeline)), - if (ownsAlbum) ...[ - const ArchiveActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), - ], - const DownloadActionButton(source: ActionSource.timeline), - if (ownsAlbum) ...[ - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), - ], + if (ownsAlbum) ...const [ + .new(action: ArchiveAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), + ], + const .new(action: DownloadAction(source: .timeline)), + if (ownsAlbum) ...const [ + .new(action: DeleteAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + ], + const .new(action: CleanupLocalAction(source: .timeline)), + if (ownsAlbum) ...[ + ActionColumnButton( + action: RemoveFromAlbumAction(source: .timeline, albumId: widget.album.id), + ), + ActionColumnButton( + action: SetAlbumCoverAction(source: .timeline, albumId: widget.album.id), + ), ], - if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), - if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), - if (ownsAlbum && multiselect.selectedAssets.length == 1) - SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), ], slivers: ownsAlbum ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand)] diff --git a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart index c96e680966..b496503dad 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; -class TrashBottomBar extends ConsumerWidget { +class TrashBottomBar extends StatelessWidget { const TrashBottomBar({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return Align( alignment: Alignment.bottomCenter, child: Container( @@ -19,9 +18,9 @@ class TrashBottomBar extends ConsumerWidget { top: false, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - DeleteTrashActionButton(source: ActionSource.timeline), - RestoreTrashActionButton(source: ActionSource.timeline), + children: [ + .new(action: DeleteAction(source: .timeline)), + .new(action: RestoreAction(source: .timeline)), ], ), ), 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 9727ec29e4..d748452c0e 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,13 +1,14 @@ +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'; -import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_placeholder.widget.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_placeholder.widget.dart'; Future showFeatureMessageDialog(BuildContext context) { return showGeneralDialog( @@ -60,7 +61,7 @@ class _FeatureMessageDialogState extends State<_FeatureMessageDialog> with Singl Navigator.of(context).pop(); return; } - _controller.nextPage(duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic); + unawaited(_controller.nextPage(duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic)); } List _borderColors(BuildContext context) { diff --git a/mobile/lib/presentation/widgets/images/full_image.widget.dart b/mobile/lib/presentation/widgets/images/full_image.widget.dart index 77ea996b89..8c92ac2818 100644 --- a/mobile/lib/presentation/widgets/images/full_image.widget.dart +++ b/mobile/lib/presentation/widgets/images/full_image.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; @@ -22,7 +24,7 @@ class FullImage extends StatelessWidget { Widget build(BuildContext context) { final provider = getFullImageProvider(asset, size: size); return OctoImage( - fadeInDuration: const Duration(milliseconds: 0), + fadeInDuration: Duration.zero, fadeOutDuration: const Duration(milliseconds: 100), placeholderBuilder: placeholder != null ? (_) => placeholder! : null, image: provider, @@ -30,7 +32,7 @@ class FullImage extends StatelessWidget { height: size.height, fit: fit, errorBuilder: (context, error, stackTrace) { - provider.evict(); + unawaited(provider.evict()); return const Icon(Icons.image_not_supported_outlined, size: 32); }, ); diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index b3c58314db..aaf3e0dbaf 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; @@ -25,7 +26,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide ImageInfo? getInitialImage(CancellableImageProvider provider) { final completer = CancelableCompleter(onCancel: provider.cancel); - final cachedStream = provider.resolve(const ImageConfiguration()); + final cachedStream = provider.resolve(ImageConfiguration.empty); ImageInfo? cachedImage; final listener = ImageStreamListener((image, synchronousCall) { if (synchronousCall) { @@ -43,10 +44,12 @@ mixin CancellableImageProviderMixin on CancellableImageProvide return cachedImage; } - completer.operation.valueOrCancellation().whenComplete(() { - cachedStream.removeListener(listener); - cachedOperation = null; - }); + unawaited( + completer.operation.valueOrCancellation().whenComplete(() { + cachedStream.removeListener(listener); + cachedOperation = null; + }), + ); cachedOperation = completer.operation; return null; } @@ -138,7 +141,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide final operation = cachedOperation; if (operation != null) { cachedOperation = null; - operation.cancel(); + unawaited(operation.cancel()); } if (hasActiveWork) { @@ -159,7 +162,15 @@ ImageProvider getFullImageProvider( provider = FileImage(File(localFilePath)); } else if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; - provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type, isAnimated: asset.isAnimatedImage); + provider = LocalFullImageProvider( + id: id, + size: size, + assetType: asset.type, + isAnimated: asset.isAnimatedImage, + width: asset.width, + height: asset.height, + checksum: asset.checksum, + ); } else { final String assetId; final String thumbhash; @@ -187,7 +198,7 @@ ImageProvider getFullImageProvider( ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, 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); + return LocalThumbProvider(id: id, size: size, assetType: asset.type, checksum: asset.checksum); } final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; diff --git a/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart index b519da33c3..966e070872 100644 --- a/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart @@ -14,7 +14,7 @@ class LocalAlbumThumbnail extends ConsumerWidget { return localAlbumThumbnail.when( data: (data) { if (data == null) { - return Container( + return DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(16)), diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index eba4f0a1cd..46e8eda85b 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -8,13 +10,19 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; +// iOS GPU textures max out at 16384px; larger images squish. +const _kMaxPixelSize = 16384; + class LocalThumbProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String id; final Size size; final AssetType assetType; - LocalThumbProvider({required this.id, required this.assetType, this.size = kThumbnailResolution}); + // an edit on the device keeps the id and changes the bytes, so the checksum is what separates two renders + final String? checksum; + + LocalThumbProvider({required this.id, required this.assetType, this.checksum, this.size = kThumbnailResolution}); @override Future obtainKey(ImageConfiguration configuration) { @@ -44,13 +52,13 @@ class LocalThumbProvider extends CancellableImageProvider return true; } if (other is LocalThumbProvider) { - return id == other.id; + return id == other.id && checksum == other.checksum; } return false; } @override - int get hashCode => id.hashCode; + int get hashCode => Object.hash(id, checksum); } class LocalFullImageProvider extends CancellableImageProvider @@ -59,8 +67,38 @@ class LocalFullImageProvider extends CancellableImageProvider + previewTargetSize(size.width * dpr, size.height * dpr, width, height, previewIsFinal: previewIsFinal); + + // Use an aspect-correct target when aspectFill would exceed the texture limit. + @visibleForTesting + static Size previewTargetSize(double boxW, double boxH, int? width, int? height, {required bool previewIsFinal}) { + if (width == null || height == null || width <= 0 || height <= 0) { + return Size(boxW, boxH); + } + final imgLong = math.max(width, height).toDouble(); + final coverLong = imgLong * math.max(boxW / width, boxH / height); + if (coverLong <= _kMaxPixelSize) { + return Size(boxW, boxH); + } + final bound = previewIsFinal ? _kMaxPixelSize.toDouble() : math.max(boxW, boxH); + final scale = math.min(1.0, bound / imgLong); + return Size(math.max(1.0, width * scale), math.max(1.0, height * scale)); + } @override Future obtainKey(ImageConfiguration configuration) { @@ -73,7 +111,7 @@ class LocalFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Id', key.id), @@ -86,7 +124,7 @@ class LocalFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Id', key.id), @@ -108,7 +146,7 @@ class LocalFullImageProvider extends CancellableImageProvider id.hashCode ^ size.hashCode ^ isAnimated.hashCode; + int get hashCode => Object.hash(id, size, isAnimated, width, height, checksum); } diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 847fa4e381..90bb79cced 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -130,9 +131,9 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix if ((synchronousCall && _providerImage == null) || !_isVisible()) { _fadeController.value = 1.0; } else if (_fadeController.isAnimating) { - _fadeController.forward(); + unawaited(_fadeController.forward()); } else { - _fadeController.forward(from: 0.0); + unawaited(_fadeController.forward(from: 0.0)); } setState(() { diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index c42d365464..1d3dcb2cf0 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -285,7 +285,7 @@ class _TileOverlayIcon extends StatelessWidget { icon, color: Colors.white, size: 16, - shadows: [const Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], + shadows: const [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset.zero)], ); } } @@ -346,7 +346,7 @@ class _UploadProgressOverlay extends StatelessWidget { final percentage = isError ? 0 : (progress * 100).toInt(); return Positioned.fill( - child: Container( + child: ColoredBox( color: isError ? Colors.red.withValues(alpha: 0.6) : Colors.black54, child: Center( child: Column( diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index f1b4f80ec1..eedf3aadf5 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; @@ -87,32 +89,32 @@ class MapStateNotifier extends Notifier { } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); + unawaited(ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly)); state = state.copyWith(onlyFavorites: isFavoriteOnly); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); + unawaited(ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived)); state = state.copyWith(includeArchived: isIncludeArchived); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchWithPartners(bool isWithPartners) { - ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); + unawaited(ref.read(settingsProvider).write(.mapWithPartners, isWithPartners)); state = state.copyWith(withPartners: isWithPartners); EventStream.shared.emit(const MapMarkerReloadEvent()); } void setRelativeTime(int relativeDays) { - ref.read(settingsProvider).write(.mapRelativeDate, relativeDays); + unawaited(ref.read(settingsProvider).write(.mapRelativeDate, relativeDays)); state = state.copyWith(relativeDays: relativeDays); EventStream.shared.emit(const MapMarkerReloadEvent()); } void setCustomTimeRange(TimeRange range) { - ref.read(settingsProvider).write(.mapCustomFrom, range.from); - ref.read(settingsProvider).write(.mapCustomTo, range.to); + unawaited(ref.read(settingsProvider).write(.mapCustomFrom, range.from)); + unawaited(ref.read(settingsProvider).write(.mapCustomTo, range.to)); state = state.copyWith(timeRange: range); EventStream.shared.emit(const MapMarkerReloadEvent()); } diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index a68a475ad4..918d64054b 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -66,7 +66,7 @@ class _DriftMapState extends ConsumerState { void dispose() { _debouncer.dispose(); bottomSheetOffset.dispose(); - _eventSubscription?.cancel(); + unawaited(_eventSubscription?.cancel()); super.dispose(); } diff --git a/mobile/lib/presentation/widgets/map/map_utils.dart b/mobile/lib/presentation/widgets/map/map_utils.dart index 80df5995b6..34116b144b 100644 --- a/mobile/lib/presentation/widgets/map/map_utils.dart +++ b/mobile/lib/presentation/widgets/map/map_utils.dart @@ -71,7 +71,11 @@ class MapUtils { bool silent = false, }) async { try { - bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + if (!serviceEnabled && !silent) { unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context))); return (null, LocationPermission.deniedForever); @@ -81,6 +85,10 @@ class MapUtils { bool shouldRequestPermission = false; if (permission == LocationPermission.denied && !silent) { + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + shouldRequestPermission = await showDialog( context: context, builder: (context) => _LocationPermissionDisabledDialog(context), @@ -98,7 +106,7 @@ class MapUtils { return (null, LocationPermission.deniedForever); } - Position currentUserLocation = await Geolocator.getCurrentPosition( + final Position currentUserLocation = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index 2f7a616632..7e782a3db4 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -55,7 +55,7 @@ class DriftMemoryCard extends StatelessWidget { } if (asset.isImage) { - return FullImage(asset, fit: fit, size: const Size(double.infinity, double.infinity)); + return FullImage(asset, fit: fit, size: Size.infinite); } return Center( @@ -97,7 +97,7 @@ class _BlurredBackdrop extends HookWidget { final blurhash = useDriftBlurHashRef(asset).value; if (blurhash != null) { // Use a nice cheap blur hash image decoration - return Container( + return DecoratedBox( decoration: BoxDecoration( image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), ), @@ -109,7 +109,7 @@ class _BlurredBackdrop extends HookWidget { // safely use that as the image provider return ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( image: DecorationImage( image: getFullImageProvider(asset, size: Size(context.width, context.height)), diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index 62889b10cb..94a91437b0 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,7 +35,7 @@ class DriftMemoryLane extends ConsumerWidget { if (memories[index].assets.isNotEmpty) { DriftMemoryPage.setMemory(ref, memories[index]); } - context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index)); + unawaited(context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index))); }, children: memories .map((memory) => DriftMemoryCard(key: Key(memory.id), memory: memory)) @@ -43,13 +45,13 @@ class DriftMemoryLane extends ConsumerWidget { } } -class DriftMemoryCard extends ConsumerWidget { +class DriftMemoryCard extends StatelessWidget { const DriftMemoryCard({super.key, required this.memory}); final DriftMemory memory; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final yearsAgo = DateTime.now().year - memory.data.year; final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()}); return Center( 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 c194bbc684..b5a9dd235c 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 @@ -6,9 +6,9 @@ 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/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'; -import 'package:immich_mobile/utils/debug_print.dart'; class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { final DriftPerson person; @@ -28,18 +28,22 @@ 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; + } + context.pop(_selectedDate); } } catch (error) { dPrint(() => 'Error updating birthday: $error'); - if (!context.mounted) { + if (!mounted) { return; } 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 6de19000e0..bb63de3695 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 @@ -6,8 +6,8 @@ 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/providers/infrastructure/people.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.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; @@ -27,17 +27,21 @@ 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; + } + context.pop(newName); } } catch (error) { dPrint(() => 'Error updating name: $error'); - if (!context.mounted) { + if (!mounted) { return; } 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 b374d48417..ac2d4580c9 100644 --- a/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -class PersonOptionSheet extends ConsumerWidget { +class PersonOptionSheet extends StatelessWidget { const PersonOptionSheet({super.key, this.onEditName, this.onEditBirthday, this.birthdayExists = false}); final VoidCallback? onEditName; @@ -10,8 +9,8 @@ class PersonOptionSheet extends ConsumerWidget { final bool birthdayExists; @override - Widget build(BuildContext context, WidgetRef ref) { - TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600); + Widget build(BuildContext context) { + final TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600); return SafeArea( child: Padding( 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 355e1a01a8..cd94ef89a8 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,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -class DriftRemoteAlbumOption extends ConsumerWidget { +class DriftRemoteAlbumOption extends StatelessWidget { const DriftRemoteAlbumOption({ super.key, this.onAddPhotos, @@ -31,7 +30,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget { final List? iconShadows; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final theme = context.themeData; final menuChildren = []; diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index 7712983fde..b108acde3b 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -107,7 +107,7 @@ class _FixedSegmentRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing)); - final timelineService = ref.read(timelineServiceProvider); + final timelineService = ref.watch(timelineServiceProvider); final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3); if (timelineService.hasRange(assetIndex, assetCount)) { @@ -211,6 +211,10 @@ class _AssetTileWidget extends ConsumerWidget { ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset); } else { await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1); + if (!ctx.mounted) { + return; + } + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; AssetViewer.setAsset(ref, asset); unawaited( @@ -257,7 +261,7 @@ class _AssetTileWidget extends ConsumerWidget { final lockSelection = _getLockSelectionStatus(ref); final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator)); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); - final showStackIndicator = ref.read(timelineServiceProvider).origin != TimelineOrigin.trash; + final showStackIndicator = ref.watch(timelineServiceProvider).origin != TimelineOrigin.trash; return RepaintBoundary( child: GestureDetector( diff --git a/mobile/lib/presentation/widgets/timeline/header.widget.dart b/mobile/lib/presentation/widgets/timeline/header.widget.dart index 3eff305251..433118d13c 100644 --- a/mobile/lib/presentation/widgets/timeline/header.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/header.widget.dart @@ -1,16 +1,19 @@ +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/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -class TimelineHeader extends HookConsumerWidget { +class TimelineHeader extends HookWidget { final Bucket bucket; final HeaderType header; final double height; @@ -37,7 +40,7 @@ class TimelineHeader extends HookConsumerWidget { } @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { if (bucket is! TimeBucket || header == HeaderType.none) { return const SizedBox.shrink(); } @@ -105,7 +108,7 @@ class _BulkSelectIconButton extends ConsumerWidget { ? const SizedBox.shrink() : IconButton( onPressed: () { - ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount); + unawaited(ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount)); ref.read(hapticFeedbackProvider.notifier).heavyImpact(); }, icon: isAllSelected diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index 27f523a2a8..eb081a1e6a 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -152,7 +152,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi void _resetThumbTimer() { _fadeOutTimer?.cancel(); _fadeOutTimer = Timer(kTimelineScrubberFadeOutDuration, () { - _thumbAnimationController.reverse(); + unawaited(_thumbAnimationController.reverse()); _fadeOutTimer = null; }); } @@ -177,10 +177,10 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi if (notification is ScrollUpdateNotification) { _thumbTopOffset = _currentOffset; if (_labelAnimation.status != AnimationStatus.reverse) { - _labelAnimationController.reverse(); + unawaited(_labelAnimationController.reverse()); } if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); + unawaited(_thumbAnimationController.forward()); } } _resetThumbTimer(); @@ -210,7 +210,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi void _onDragStart(DragStartDetails _) { setState(() { _isDragging = true; - _labelAnimationController.forward(); + unawaited(_labelAnimationController.forward()); _fadeOutTimer?.cancel(); _lastLabel = null; }); @@ -226,7 +226,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi } if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); + unawaited(_thumbAnimationController.forward()); } final dragPosition = _calculateDragPosition(details); @@ -344,7 +344,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi } void _onDragEnd(DragEndDetails _) { - _labelAnimationController.reverse(); + unawaited(_labelAnimationController.reverse()); setState(() { _isDragging = false; }); @@ -590,7 +590,7 @@ class _SlideFadeTransition extends StatelessWidget { animation: _animation, builder: (context, child) => _animation.value == 0.0 ? const SizedBox() : child!, child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(_animation), + position: Tween(begin: const Offset(0.3, 0.0), end: Offset.zero).animate(_animation), child: FadeTransition(opacity: _animation, child: _child), ), ); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index b39c431b43..bcdc3b4783 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -1,5 +1,7 @@ import 'dart:math' as math; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; @@ -8,6 +10,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +part 'timeline.state.freezed.dart'; + class TimelineArgs { final double maxWidth; final double maxHeight; @@ -49,25 +53,13 @@ class TimelineArgs { groupBy.hashCode; } -class TimelineState { - final bool isScrubbing; - final bool isScrolling; +@freezed +abstract class TimelineState with _$TimelineState { + const TimelineState._(); - const TimelineState({this.isScrubbing = false, this.isScrolling = false}); + const factory TimelineState({@Default(false) bool isScrubbing, @Default(false) bool isScrolling}) = _TimelineState; bool get isInteracting => isScrubbing || isScrolling; - - @override - bool operator ==(covariant TimelineState other) { - return isScrubbing == other.isScrubbing && isScrolling == other.isScrolling; - } - - @override - int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode; - - TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) { - return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing, isScrolling: isScrolling ?? this.isScrolling); - } } class TimelineStateNotifier extends Notifier { diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 20a4b4257e..9e65dcb72c 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -199,7 +199,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi switch (event) { case ScrollToTopEvent(): _scrollToTop(); - case ScrollToDateEvent scrollToDateEvent: + case final ScrollToDateEvent scrollToDateEvent: _scrollToDate(scrollToDateEvent.date); case TimelineReloadEvent(): setState(() {}); @@ -258,7 +258,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi void dispose() { WidgetsBinding.instance.removeObserver(this); _scrollController.dispose(); - _eventSubscription?.cancel(); + unawaited(_eventSubscription?.cancel()); super.dispose(); } @@ -269,9 +269,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi final timelineState = ref.read(timelineStateProvider.notifier); timelineState.setScrubbing(true); - _scrollController - .animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut) - .whenComplete(() => timelineState.setScrubbing(false)); + unawaited( + _scrollController + .animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut) + .whenComplete(() => timelineState.setScrubbing(false)), + ); } void _scrollToDate(DateTime date) { @@ -303,13 +305,15 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi // Scroll to the segment with a small offset to show the header final targetOffset = fallbackSegment.startOffset - 50; timelineState.setScrubbing(true); - _scrollController - .animateTo( - targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - .whenComplete(() => timelineState.setScrubbing(false)); + unawaited( + _scrollController + .animateTo( + targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + .whenComplete(() => timelineState.setScrubbing(false)), + ); } else { timelineState.setScrubbing(false); } @@ -344,8 +348,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi }); } - void _dragScroll(ScrollDirection direction) { - _scrollController.animateTo( + Future _dragScroll(ScrollDirection direction) { + return _scrollController.animateTo( _scrollController.offset + (direction == ScrollDirection.forward ? 175 : -175), duration: const Duration(milliseconds: 125), curve: Curves.easeOut, @@ -488,7 +492,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi _restoreAssetIndex = targetAssetIndex; }); - ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow); + unawaited(ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow)); } }; }, @@ -498,7 +502,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null, onAssetEnter: _handleDragAssetEnter, onEnd: !isReadonlyModeEnabled ? _stopDrag : null, - onScroll: _dragScroll, + onScroll: (direction) => unawaited(_dragScroll(direction)), onScrollStart: () { // Minimize the bottom sheet when drag selection starts ref.read(timelineStateProvider.notifier).setScrolling(true); diff --git a/mobile/lib/providers/album/album_title.provider.dart b/mobile/lib/providers/album/album_title.provider.dart index bf812a01d8..b38c2929fb 100644 --- a/mobile/lib/providers/album/album_title.provider.dart +++ b/mobile/lib/providers/album/album_title.provider.dart @@ -3,11 +3,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class AlbumTitleNotifier extends StateNotifier { AlbumTitleNotifier() : super(""); - setAlbumTitle(String title) { + void setAlbumTitle(String title) { state = title; } - clearAlbumTitle() { + void clearAlbumTitle() { state = ""; } } diff --git a/mobile/lib/providers/album/current_album.provider.g.dart b/mobile/lib/providers/album/current_album.provider.g.dart deleted file mode 100644 index b6d079231f..0000000000 --- a/mobile/lib/providers/album/current_album.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_album.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentAlbumHash() => r'61f00273d6b69da45add1532cc3d3a076ee55110'; - -/// See also [CurrentAlbum]. -@ProviderFor(CurrentAlbum) -final currentAlbumProvider = - AutoDisposeNotifierProvider.internal( - CurrentAlbum.new, - name: r'currentAlbumProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentAlbumHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$CurrentAlbum = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index ad0940b776..7dc475ab93 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -35,7 +35,7 @@ class AppLifeCycleNotifier extends StateNotifier { return state; } - void handleAppResume() async { + Future handleAppResume() async { state = AppLifeCycleEnum.resumed; // Prevent overlapping resume operations @@ -81,6 +81,10 @@ class AppLifeCycleNotifier extends StateNotifier { await _ref.read(serverInfoProvider.notifier).getServerVersion(); } + if (!_shouldContinueOperation()) { + _wasPaused = true; + return; + } _ref.read(websocketProvider.notifier).connect(); await _handleBetaTimelineResume(); @@ -89,13 +93,13 @@ class AppLifeCycleNotifier extends StateNotifier { await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); } - Future _safeRun(Future action, String debugName) async { + Future _safeRun(Future Function() action, String debugName) async { if (!_shouldContinueOperation()) { return; } try { - await action; + await action(); } catch (e, stackTrace) { _log.warning("Error during $debugName operation", e, stackTrace); } @@ -113,25 +117,27 @@ class AppLifeCycleNotifier extends StateNotifier { try { bool syncSuccess = false; await Future.wait([ - _safeRun(backgroundManager.syncLocal(full: CurrentPlatform.isAndroid ? true : false), "syncLocal"), - _safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"), + _safeRun(() => backgroundManager.syncLocal(full: CurrentPlatform.isAndroid), "syncLocal"), + _safeRun(() async { + syncSuccess = await backgroundManager.syncRemote(); + }, "syncRemote"), ]); _ref.invalidate(driftMemoryFutureProvider); if (syncSuccess) { await Future.wait([ - _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { - _resumeBackup(); + _safeRun(backgroundManager.hashAssets, "hashAssets").then((_) { + unawaited(_resumeBackup()); }), _resumeBackup(), // TODO: Bring back when the soft freeze issue is addressed // _safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"), ]); } else { - await _safeRun(backgroundManager.hashAssets(), "hashAssets"); + await _safeRun(backgroundManager.hashAssets, "hashAssets"); } if (isAlbumLinkedSyncEnable) { - await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum"); + await _safeRun(backgroundManager.syncLinkedAlbum, "syncLinkedAlbum"); } } catch (e, stackTrace) { _log.severe("Error during background sync", e, stackTrace); @@ -145,7 +151,7 @@ class AppLifeCycleNotifier extends StateNotifier { final currentUser = Store.tryGet(StoreKey.currentUser); if (currentUser != null) { await _safeRun( - _ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id), + () => _ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id), "handleBackupResume", ); } @@ -195,7 +201,7 @@ class AppLifeCycleNotifier extends StateNotifier { Future _performPause() { if (_ref.read(authProvider).isAuthenticated) { - _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); + _ref.read(driftBackupProvider.notifier).stopForegroundBackup(reason: "the app being sent to the background"); _ref.read(websocketProvider.notifier).disconnect(); } diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index 6808860ffc..fd2a6aebd9 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -1,81 +1,25 @@ import 'dart:async'; +import 'package:flutter/foundation.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/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -class AssetViewerState { - final double backgroundOpacity; - final bool showingDetails; - final bool showingControls; - final bool isZoomed; - final bool showingOcr; - final BaseAsset? currentAsset; - final int stackIndex; +part 'asset_viewer.provider.freezed.dart'; - const AssetViewerState({ - this.backgroundOpacity = 1.0, - this.showingDetails = false, - this.showingControls = true, - this.isZoomed = false, - this.showingOcr = false, - this.currentAsset, - this.stackIndex = 0, - }); - - AssetViewerState copyWith({ - double? backgroundOpacity, - bool? showingDetails, - bool? showingControls, - bool? isZoomed, - bool? showingOcr, +@freezed +abstract class AssetViewerState with _$AssetViewerState { + const factory AssetViewerState({ + @Default(1.0) double backgroundOpacity, + @Default(false) bool showingDetails, + @Default(true) bool showingControls, + @Default(false) bool isZoomed, + @Default(false) bool showingOcr, BaseAsset? currentAsset, - int? stackIndex, - }) { - return AssetViewerState( - backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity, - showingDetails: showingDetails ?? this.showingDetails, - showingControls: showingControls ?? this.showingControls, - isZoomed: isZoomed ?? this.isZoomed, - showingOcr: showingOcr ?? this.showingOcr, - currentAsset: currentAsset ?? this.currentAsset, - stackIndex: stackIndex ?? this.stackIndex, - ); - } - - @override - String toString() { - return 'AssetViewerState(opacity: $backgroundOpacity, showingDetails: $showingDetails, controls: $showingControls, isZoomed: $isZoomed, showingOcr: $showingOcr)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - return other is AssetViewerState && - other.backgroundOpacity == backgroundOpacity && - other.showingDetails == showingDetails && - other.showingControls == showingControls && - other.isZoomed == isZoomed && - other.showingOcr == showingOcr && - other.currentAsset == currentAsset && - other.stackIndex == stackIndex; - } - - @override - int get hashCode => - backgroundOpacity.hashCode ^ - showingDetails.hashCode ^ - showingControls.hashCode ^ - isZoomed.hashCode ^ - showingOcr.hashCode ^ - currentAsset.hashCode ^ - stackIndex.hashCode; + @Default(0) int stackIndex, + }) = _AssetViewerState; } class AssetViewerStateNotifier extends Notifier { @@ -88,7 +32,7 @@ class AssetViewerStateNotifier extends Notifier { } void reset() { - _assetSubscription?.cancel(); + unawaited(_assetSubscription?.cancel()); _assetSubscription = null; state = const AssetViewerState(); } @@ -102,7 +46,7 @@ class AssetViewerStateNotifier extends Notifier { } void _watchCurrentAsset(BaseAsset asset) { - _assetSubscription?.cancel(); + unawaited(_assetSubscription?.cancel()); _assetSubscription = ref.read(assetServiceProvider).watchAsset(asset).listen((updated) { if (updated != null) { state = state.copyWith(currentAsset: updated); diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index 37d3392d88..2c4854bdb0 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -37,7 +37,7 @@ class DownloadStateNotifier extends StateNotifier { ); } - void cancelDownload(String id) async { + Future cancelDownload(String id) async { final isCanceled = await _downloadService.cancelDownload(id); if (isCanceled) { @@ -55,5 +55,5 @@ class DownloadStateNotifier extends StateNotifier { } final downloadStateProvider = StateNotifierProvider( - ((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))), + (ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider)), ); diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart index 8bd0581061..47d67c9674 100644 --- a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,11 +10,11 @@ import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; final shareIntentUploadProvider = StateNotifierProvider>( - ((ref) => ShareIntentUploadStateNotifier( + (ref) => ShareIntentUploadStateNotifier( ref.watch(appRouterProvider), ref.read(foregroundUploadServiceProvider), ref.read(shareIntentServiceProvider), - )), + ), ); class ShareIntentUploadStateNotifier extends StateNotifier> { @@ -33,7 +34,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier route.name == "ShareIntentRoute"); clearAttachments(); addAttachments(attachments); - router.push(ShareIntentRoute(attachments: attachments)); + unawaited(router.push(ShareIntentRoute(attachments: attachments))); } void addAttachments(List attachments) { diff --git a/mobile/lib/providers/asset_viewer/video_player_provider.dart b/mobile/lib/providers/asset_viewer/video_player_provider.dart index 463a1ac3d2..74d697a2ea 100644 --- a/mobile/lib/providers/asset_viewer/video_player_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_provider.dart @@ -50,7 +50,7 @@ class VideoPlayerNotifier extends StateNotifier { void dispose() { _bufferingTimer?.cancel(); _seekTimer?.cancel(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); _controller = null; super.dispose(); @@ -121,7 +121,7 @@ class VideoPlayerNotifier extends StateNotifier { } _seekTimer = Timer(const Duration(milliseconds: 150), () { - _controller?.seekTo(state.position.inMilliseconds); + unawaited(_controller?.seekTo(state.position.inMilliseconds)); }); } @@ -130,11 +130,11 @@ class VideoPlayerNotifier extends StateNotifier { switch (state.status) { case VideoPlaybackStatus.paused: - play(); + unawaited(play()); case VideoPlaybackStatus.playing || VideoPlaybackStatus.buffering: - pause(); + unawaited(pause()); case VideoPlaybackStatus.completed: - restart(); + unawaited(restart()); } } @@ -145,7 +145,7 @@ class VideoPlayerNotifier extends StateNotifier { } _holdStatus = state.status; - pause(); + unawaited(pause()); } /// Restores playback to the status before [hold] was called. @@ -155,7 +155,7 @@ class VideoPlayerNotifier extends StateNotifier { switch (status) { case VideoPlaybackStatus.playing || VideoPlaybackStatus.buffering: - play(); + unawaited(play()); default: } } @@ -238,7 +238,7 @@ class VideoPlayerNotifier extends StateNotifier { final newStatus = _mapStatus(playbackInfo.status); switch (newStatus) { case VideoPlaybackStatus.playing: - WakelockPlus.enable(); + unawaited(WakelockPlus.enable()); _startBufferingTimer(); default: onNativePlaybackEnded(); @@ -250,7 +250,7 @@ class VideoPlayerNotifier extends StateNotifier { } void onNativePlaybackEnded() { - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); _bufferingTimer?.cancel(); } diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 23ccea8025..2ed4cbcf00 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -134,7 +134,7 @@ class AuthNotifier extends StateNotifier { await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); // Get the deviceid from the store if it exists, otherwise generate a new one - String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; + final String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; UserDto? user = _userService.tryGetMyUser(); diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart index 37b3145eb4..5873e992b1 100644 --- a/mobile/lib/providers/background_sync.provider.dart +++ b/mobile/lib/providers/background_sync.provider.dart @@ -9,6 +9,7 @@ final backgroundSyncProvider = Provider((ref) { final manager = BackgroundSyncManager( onRemoteSyncStart: () { syncStatusNotifier.startRemoteSync(); + // ignore: dispose-provided-instances final backupProvider = ref.read(driftBackupProvider.notifier); if (backupProvider.mounted) { backupProvider.updateError(BackupError.none); @@ -16,6 +17,7 @@ final backgroundSyncProvider = Provider((ref) { }, onRemoteSyncComplete: (isSuccess) { syncStatusNotifier.completeRemoteSync(); + // ignore: dispose-provided-instances final backupProvider = ref.read(driftBackupProvider.notifier); if (backupProvider.mounted) { backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed); diff --git a/mobile/lib/providers/backup/backup_album.provider.dart b/mobile/lib/providers/backup/backup_album.provider.dart index f81f905c2f..25a4204928 100644 --- a/mobile/lib/providers/backup/backup_album.provider.dart +++ b/mobile/lib/providers/backup/backup_album.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/local_album.service.dart'; @@ -10,7 +12,7 @@ final backupAlbumProvider = StateNotifierProvider> { BackupAlbumNotifier(this._localAlbumService) : super([]) { - getAll(); + unawaited(getAll()); } final LocalAlbumService _localAlbumService; diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index bf2b7cae4a..1756d0fd30 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -1,17 +1,19 @@ import 'dart:async'; -import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:logging/logging.dart'; - import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/utils/upload_speed_calculator.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/utils/upload_speed_calculator.dart'; +import 'package:logging/logging.dart'; + +part 'drift_backup.provider.freezed.dart'; class EnqueueStatus { final int enqueueCount; @@ -27,75 +29,17 @@ class EnqueueStatus { String toString() => 'EnqueueStatus(enqueueCount: $enqueueCount, totalCount: $totalCount)'; } -class DriftUploadStatus { - final String taskId; - final String filename; - final double progress; - final int fileSize; - final String networkSpeedAsString; - final bool? isFailed; - final String? error; - - const DriftUploadStatus({ - required this.taskId, - required this.filename, - required this.progress, - required this.fileSize, - required this.networkSpeedAsString, - this.isFailed, - this.error, - }); - - DriftUploadStatus copyWith({ - String? taskId, - String? filename, - double? progress, - int? fileSize, - String? networkSpeedAsString, +@freezed +abstract class DriftUploadStatus with _$DriftUploadStatus { + const factory DriftUploadStatus({ + required String taskId, + required String filename, + required double progress, + required int fileSize, + required String networkSpeedAsString, bool? isFailed, String? error, - }) { - return DriftUploadStatus( - taskId: taskId ?? this.taskId, - filename: filename ?? this.filename, - progress: progress ?? this.progress, - fileSize: fileSize ?? this.fileSize, - networkSpeedAsString: networkSpeedAsString ?? this.networkSpeedAsString, - isFailed: isFailed ?? this.isFailed, - error: error ?? this.error, - ); - } - - @override - String toString() { - return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString, isFailed: $isFailed, error: $error)'; - } - - @override - bool operator ==(covariant DriftUploadStatus other) { - if (identical(this, other)) { - return true; - } - - return other.taskId == taskId && - other.filename == filename && - other.progress == progress && - other.fileSize == fileSize && - other.networkSpeedAsString == networkSpeedAsString && - other.isFailed == isFailed && - other.error == error; - } - - @override - int get hashCode { - return taskId.hashCode ^ - filename.hashCode ^ - progress.hashCode ^ - fileSize.hashCode ^ - networkSpeedAsString.hashCode ^ - isFailed.hashCode ^ - error.hashCode; - } + }) = _DriftUploadStatus; } enum BackupError { none, syncFailed } @@ -256,19 +200,25 @@ class DriftBackupNotifier extends StateNotifier { state = state.copyWith(isSyncing: isSyncing); } - Future startForegroundBackup(String userId) { + Future startForegroundBackup(String userId) async { // Cancel any existing backup before starting a new one if (_cancelToken != null) { - stopForegroundBackup(); + stopForegroundBackup(reason: "restarting the backup"); } state = state.copyWith(error: BackupError.none); - _cancelToken = Completer(); + // A pause during the recount below nulls _cancelToken, so the run keeps its own reference. + final cancelToken = Completer(); + _cancelToken = cancelToken; + + // Re-baseline the counters against the same DB read that feeds this run's candidate list, + // otherwise a resume counts duplicate successes against the old baseline (#26215). + await getBackupStatus(userId); return _foregroundUploadService.uploadCandidates( userId, - _cancelToken!, + cancelToken, callbacks: UploadCallbacks( onProgress: _handleForegroundBackupProgress, onSuccess: _handleForegroundBackupSuccess, @@ -278,7 +228,10 @@ class DriftBackupNotifier extends StateNotifier { ); } - void stopForegroundBackup() { + void stopForegroundBackup({required String reason}) { + if (_cancelToken != null) { + _logger.info("Foreground backup cancelled: $reason"); + } _cancelToken?.complete(); _cancelToken = null; _uploadSpeedManager.clear(); @@ -334,6 +287,10 @@ class DriftBackupNotifier extends StateNotifier { } void _handleForegroundBackupSuccess(String localAssetId, String remoteAssetId) { + if (!mounted) { + _logger.warning("Skip _handleForegroundBackupSuccess: notifier disposed"); + return; + } state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1); _uploadSpeedManager.removeTask(localAssetId); diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index b298514d67..776888146b 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; @@ -51,14 +53,13 @@ class CastNotifier extends StateNotifier { } void loadMedia(RemoteAsset asset, bool reload) { - _gCastService.loadMedia(asset, reload); + unawaited(_gCastService.loadMedia(asset, reload)); } Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: await _gCastService.connect(device); - break; } } diff --git a/mobile/lib/providers/cleanup.provider.dart b/mobile/lib/providers/cleanup.provider.dart index 378ceb010f..4316b4eb00 100644 --- a/mobile/lib/providers/cleanup.provider.dart +++ b/mobile/lib/providers/cleanup.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + 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'; @@ -87,18 +89,18 @@ class CleanupNotifier extends StateNotifier { state = state.copyWith(selectedDate: date, assetsToDelete: []); if (date != null) { final daysAgo = DateTime.now().difference(date).inDays; - _settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo); + unawaited(_settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo)); } } void setKeepMediaType(AssetKeepType keepMediaType) { state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []); - _settingsRepository.write(.cleanupKeepMediaType, keepMediaType); + unawaited(_settingsRepository.write(.cleanupKeepMediaType, keepMediaType)); } void setKeepFavorites(bool keepFavorites) { state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []); - _settingsRepository.write(.cleanupKeepFavorites, keepFavorites); + unawaited(_settingsRepository.write(.cleanupKeepFavorites, keepFavorites)); } void toggleKeepAlbum(String albumId) { @@ -118,7 +120,7 @@ class CleanupNotifier extends StateNotifier { } void _persistExcludedAlbumIds(Set albumIds) { - _settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList()); + unawaited(_settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList())); } void cleanupStaleAlbumIds(Set existingAlbumIds) { @@ -144,7 +146,7 @@ class CleanupNotifier extends StateNotifier { _persistExcludedAlbumIds(keepAlbumIds); } - _settingsRepository.write(.cleanupDefaultsInitialized, true); + unawaited(_settingsRepository.write(.cleanupDefaultsInitialized, true)); } Future scanAssets() async { diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 6e4fc69926..6d4703c834 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -9,10 +10,10 @@ class GalleryPermissionNotifier extends StateNotifier { : super(PermissionStatus.denied) // Denied is the initial state { // Sets the initial state - getGalleryPermissionStatus(); + unawaited(getGalleryPermissionStatus()); } - get hasPermission => state.isGranted || state.isLimited; + bool get hasPermission => state.isGranted || state.isLimited; /// Requests the gallery permission Future requestGalleryPermission() async { diff --git a/mobile/lib/providers/haptic_feedback.provider.dart b/mobile/lib/providers/haptic_feedback.provider.dart index 711c6fa4e2..850935163d 100644 --- a/mobile/lib/providers/haptic_feedback.provider.dart +++ b/mobile/lib/providers/haptic_feedback.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; @@ -13,33 +15,33 @@ class HapticNotifier extends StateNotifier { HapticNotifier(this._ref) : super(null); - selectionClick() { + void selectionClick() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.selectionClick(); + unawaited(HapticFeedback.selectionClick()); } } - lightImpact() { + void lightImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.lightImpact(); + unawaited(HapticFeedback.lightImpact()); } } - mediumImpact() { + void mediumImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.mediumImpact(); + unawaited(HapticFeedback.mediumImpact()); } } - heavyImpact() { + void heavyImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.heavyImpact(); + unawaited(HapticFeedback.heavyImpact()); } } - vibrate() { + void vibrate() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.vibrate(); + unawaited(HapticFeedback.vibrate()); } } } diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 52e2d9e0b9..3fb9f8acbc 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use-ref-and-state-synchronously + import 'dart:async'; import 'package:auto_route/auto_route.dart'; @@ -6,26 +8,15 @@ 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/domain/models/asset_edit.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; -import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/utils/semver.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; final actionProvider = NotifierProvider(ActionNotifier.new, dependencies: [multiSelectProvider]); @@ -53,7 +44,6 @@ class ActionNotifier extends Notifier { final Logger _logger = Logger('ActionNotifier'); late ActionService _service; late ForegroundUploadService _foregroundUploadService; - late AssetService _assetService; ActionNotifier() : super(); @@ -61,56 +51,17 @@ class ActionNotifier extends Notifier { void build() { _foregroundUploadService = ref.watch(foregroundUploadServiceProvider); _service = ref.watch(actionServiceProvider); - _assetService = ref.watch(assetServiceProvider); } List _getRemoteIdsForSource(ActionSource source) { return _getAssets(source).whereType().toIds().toList(growable: false); } - List _getLocalIdsForSource(ActionSource source, {bool ignoreLocalOnly = false}) { - final Set assets = _getAssets(source); - final List localIds = []; - - for (final asset in assets) { - if (ignoreLocalOnly && asset.storage != AssetState.merged) { - continue; - } - if (asset is LocalAsset) { - localIds.add(asset.id); - } else if (asset is RemoteAsset && asset.localId != null) { - localIds.add(asset.localId!); - } - } - - return localIds; - } - - List _getOwnedRemoteIdsForSource(ActionSource source) { - final ownerId = ref.read(currentUserProvider)?.id; - return _getAssets(source).whereType().ownedAssets(ownerId).toIds().toList(growable: false); - } - - List _getOwnedRemoteAssetsForSource(ActionSource source) { - final ownerId = ref.read(currentUserProvider)?.id; - return _getIdsForSource(source).ownedAssets(ownerId).toList(); - } - - Iterable _getIdsForSource(ActionSource source) { - final Set assets = _getAssets(source); - return switch (T) { - const (RemoteAsset) => assets.whereType(), - const (LocalAsset) => assets.whereType(), - _ => const [], - } - as Iterable; - } - Set _getAssets(ActionSource source) { return switch (source) { ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, ActionSource.viewer => switch (ref.read(assetViewerProvider).currentAsset) { - BaseAsset asset => {asset}, + final BaseAsset asset => {asset}, null => const {}, }, }; @@ -126,107 +77,6 @@ class ActionNotifier extends Notifier { return ActionResult(count: assets.length, success: true); } - Future shareLink(ActionSource source, BuildContext context) async { - final ids = _getRemoteIdsForSource(source); - try { - await _service.shareLink(ids, context); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to create shared link for assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future favorite(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.favorite(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to favorite assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future unFavorite(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.unFavorite(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to unfavorite assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future archive(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.archive(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to archive assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future unArchive(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.unArchive(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to unarchive assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future moveToLockFolder(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true); - try { - await _service.moveToLockFolder(ids, localIds); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to move assets to lock folder', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future removeFromLockFolder(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.removeFromLockFolder(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to remove assets from lock folder', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future trash(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - - try { - await _service.trash(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to trash assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future restoreTrash(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.restoreTrash(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to restore trash assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future emptyTrash(String userId) async { try { final count = await _service.emptyTrash(userId); @@ -247,121 +97,6 @@ class ActionNotifier extends Notifier { } } - Future trashRemoteAndDeleteLocal(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - final localIds = _getLocalIdsForSource(source); - try { - await _service.trashRemoteAndDeleteLocal(ids, localIds); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to delete assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future deleteRemoteAndLocal(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - final localIds = _getLocalIdsForSource(source); - try { - await _service.deleteRemoteAndLocal(ids, localIds); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to delete assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future deleteLocal(ActionSource source, BuildContext context) async { - final assets = _getAssets(source); - bool? backedUpOnly = assets.every((asset) => asset.storage == AssetState.merged) - ? true - : await showDialog( - context: context, - builder: (BuildContext context) => DeleteLocalOnlyDialog(onDeleteLocal: (_) {}), - ); - - if (backedUpOnly == null) { - // User cancelled the dialog - return null; - } - - final List ids; - if (backedUpOnly) { - ids = assets.where((asset) => asset.storage == AssetState.merged).map((asset) => asset.localId!).toList(); - } else { - ids = _getLocalIdsForSource(source); - } - - try { - final deletedCount = await _service.deleteLocal(ids); - return ActionResult(count: deletedCount, success: true); - } catch (error, stack) { - _logger.severe('Failed to delete assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future editLocation(ActionSource source, BuildContext context) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - final isEdited = await _service.editLocation(ids, context); - if (!isEdited) { - return null; - } - - // This must be called since editing location - // does not update the currentAsset which means - // the exif provider will not be refreshed automatically - if (source == ActionSource.viewer) { - final currentAsset = ref.read(assetViewerProvider).currentAsset; - if (currentAsset != null) { - ref.invalidate(assetExifProvider(currentAsset)); - } - } - - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to edit location for assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future editDateTime(ActionSource source, BuildContext context) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - final isEdited = await _service.editDateTime(ids, context); - if (!isEdited) { - return null; - } - - if (source == ActionSource.viewer) { - ref.invalidate(assetExifProvider); - } - - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to edit date and time for assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future tagAssets(ActionSource source, BuildContext context) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - final count = await _service.tagAssets(ids, context); - if (count == null) { - return null; - } - - ref.invalidate(tagProvider); - return ActionResult(count: count, success: true); - } catch (error, stack) { - _logger.severe('Failed to tag assets', error, stack); - ref.invalidate(tagProvider); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future addToAlbum(ActionSource source, RemoteAlbum album) async { final selected = _getAssets(source).toList(growable: false); if (selected.isEmpty) { @@ -412,33 +147,6 @@ class ActionNotifier extends Notifier { ); } - Future removeFromAlbum(ActionSource source, String albumId) async { - final ids = _getRemoteIdsForSource(source); - try { - final removedCount = await _service.removeFromAlbum(ids, albumId); - return ActionResult(count: removedCount, success: true); - } catch (error, stack) { - _logger.severe('Failed to remove assets from album', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future setAlbumCover(ActionSource source, String albumId) async { - final assets = _getAssets(source); - final asset = assets.first; - if (asset is! RemoteAsset) { - return const ActionResult(count: 1, success: false, error: 'Asset must be remote'); - } - - try { - await _service.setAlbumCover(albumId, asset.id); - return const ActionResult(count: 1, success: true); - } catch (error, stack) { - _logger.severe('Failed to set album cover', error, stack); - return ActionResult(count: 1, success: false, error: error.toString()); - } - } - Future updateDescription(ActionSource source, String description) async { final ids = _getRemoteIdsForSource(source); if (ids.length != 1) { @@ -471,71 +179,6 @@ class ActionNotifier extends Notifier { } } - Future stack(String userId, ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.stack(userId, ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to stack assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future unStack(ActionSource source) async { - final assets = _getOwnedRemoteAssetsForSource(source); - try { - await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList()); - if (source == ActionSource.viewer) { - final updatedParent = await _assetService.getRemoteAsset(assets.first.id); - if (updatedParent != null) { - ref.read(assetViewerProvider.notifier).setAsset(updatedParent); - } - } - - return ActionResult(count: assets.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to unstack assets', error, stack); - return ActionResult(count: assets.length, success: false); - } - } - - Future shareAssets( - ActionSource source, - BuildContext context, { - ShareAssetType fileType = ShareAssetType.original, - Completer? cancelCompleter, - void Function(double progress)? onAssetDownloadProgress, - }) async { - final ids = _getAssets(source).toList(growable: false); - - try { - final count = await _service.shareAssets( - ids, - context, - fileType: fileType, - cancelCompleter: cancelCompleter, - onAssetDownloadProgress: onAssetDownloadProgress, - ); - return ActionResult(count: count, success: count > 0 || ids.isEmpty); - } catch (error, stack) { - _logger.severe('Failed to share assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future downloadAll(ActionSource source) async { - final assets = _getAssets(source).whereType().toList(growable: false); - try { - final didEnqueue = await _service.downloadAll(assets); - final enqueueCount = didEnqueue.where((e) => e).length; - return ActionResult(count: enqueueCount, success: true); - } catch (error, stack) { - _logger.severe('Failed to download assets', error, stack); - return ActionResult(count: assets.length, success: false, error: error.toString()); - } - } - Future upload( ActionSource source, { List? assets, @@ -616,46 +259,8 @@ class ActionNotifier extends Notifier { }); } } - - Future applyEdits(ActionSource source, List edits) async { - final ids = _getOwnedRemoteIdsForSource(source); - - if (ids.length != 1) { - _logger.warning('applyEdits called with multiple assets, expected single asset'); - return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits'); - } - - Future editReady; - if (ref.read(serverInfoProvider).serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)) { - editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV2", (dynamic data) { - final eventAsset = SyncAssetV2.fromJson(data["asset"]); - return eventAsset?.id == ids.first; - }, const Duration(seconds: 10)); - } else { - editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) { - final eventAsset = SyncAssetV1.fromJson(data["asset"]); - return eventAsset?.id == ids.first; - }, const Duration(seconds: 10)); - } - - try { - await _service.applyEdits(ids.first, edits); - await editReady; - return const ActionResult(count: 1, success: true); - } catch (error, stack) { - _logger.severe('Failed to apply edits to assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } } extension on Iterable { Iterable toIds() => map((e) => e.id); - - Iterable ownedAssets(String? ownerId) { - if (ownerId == null) { - return const []; - } - return whereType().where((a) => a.ownerId == ownerId); - } } diff --git a/mobile/lib/providers/infrastructure/asset.provider.dart b/mobile/lib/providers/infrastructure/asset.provider.dart index fed55208e4..8ca0ca0ec9 100644 --- a/mobile/lib/providers/infrastructure/asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset.provider.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.re import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; final localAssetRepository = Provider( (ref) => DriftLocalAssetRepository(ref.watch(driftProvider)), @@ -28,6 +29,8 @@ final assetServiceProvider = Provider( exifRepository: ref.watch(remoteExifRepositoryProvider), localRepository: ref.watch(localAssetRepository), apiRepository: ref.watch(assetApiRepositoryProvider), + mediaRepository: ref.watch(assetMediaRepositoryProvider), + trashedLocalRepository: ref.watch(trashedLocalAssetRepository), ), ); diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart index d503919c90..be94a8a341 100644 --- a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart +++ b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -16,11 +18,11 @@ class ReadOnlyModeNotifier extends Notifier { void setMode(bool value) { final isLoggedIn = ref.read(authProvider).isAuthenticated; - _appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); + unawaited(_appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value)); state = value; if (value && isLoggedIn) { - ref.read(appRouterProvider).navigate(const MainTimelineRoute()); + unawaited(ref.read(appRouterProvider).navigate(const MainTimelineRoute())); } } diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index b392f5f401..4457b9f831 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use-ref-and-state-synchronously + import 'dart:async'; import 'package:collection/collection.dart'; diff --git a/mobile/lib/providers/infrastructure/setting.provider.dart b/mobile/lib/providers/infrastructure/setting.provider.dart index 7d8be72cd0..9e33017cab 100644 --- a/mobile/lib/providers/infrastructure/setting.provider.dart +++ b/mobile/lib/providers/infrastructure/setting.provider.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use-ref-and-state-synchronously + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/services/setting.service.dart'; diff --git a/mobile/lib/providers/infrastructure/tag.provider.dart b/mobile/lib/providers/infrastructure/tag.provider.dart index 2d527768c5..152582aa93 100644 --- a/mobile/lib/providers/infrastructure/tag.provider.dart +++ b/mobile/lib/providers/infrastructure/tag.provider.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use-ref-and-state-synchronously + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/services/tag.service.dart'; diff --git a/mobile/lib/providers/infrastructure/timeline.provider.dart b/mobile/lib/providers/infrastructure/timeline.provider.dart index a67b8dd822..e5ac1389e9 100644 --- a/mobile/lib/providers/infrastructure/timeline.provider.dart +++ b/mobile/lib/providers/infrastructure/timeline.provider.dart @@ -24,7 +24,7 @@ final timelineServiceProvider = Provider( }, // Empty dependencies to inform the framework that this provider // might be used in a ProviderScope - dependencies: [], + dependencies: const [], ); final timelineFactoryProvider = Provider( diff --git a/mobile/lib/providers/infrastructure/toast.provider.dart b/mobile/lib/providers/infrastructure/toast.provider.dart index 27d1cf9e6b..eaaffd6fca 100644 --- a/mobile/lib/providers/infrastructure/toast.provider.dart +++ b/mobile/lib/providers/infrastructure/toast.provider.dart @@ -1,4 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/toast.repository.dart'; +import 'package:immich_mobile/services/toast.service.dart'; -final toastRepositoryProvider = Provider((ref) => const .new()); +final toastServiceProvider = Provider((ref) => const .new()); diff --git a/mobile/lib/providers/local_auth.provider.dart b/mobile/lib/providers/local_auth.provider.dart index 44fc5ad80c..58bd4fb0f8 100644 --- a/mobile/lib/providers/local_auth.provider.dart +++ b/mobile/lib/providers/local_auth.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,9 +23,11 @@ class LocalAuthNotifier extends StateNotifier { LocalAuthNotifier(this._localAuthService, this._secureStorageService) : super(const BiometricStatus(availableBiometrics: [], canAuthenticate: false)) { - _localAuthService.getStatus().then((value) { - state = state.copyWith(canAuthenticate: value.canAuthenticate, availableBiometrics: value.availableBiometrics); - }); + unawaited( + _localAuthService.getStatus().then((value) { + state = state.copyWith(canAuthenticate: value.canAuthenticate, availableBiometrics: value.availableBiometrics); + }), + ); } Future registerBiometric(BuildContext context, String pinCode) async { @@ -48,15 +52,12 @@ class LocalAuthNotifier extends StateNotifier { case "NotEnrolled": _log.warning("User is not enrolled in biometrics"); errorMessage = "biometric_no_options".tr(); - break; case "NotAvailable": _log.warning("Biometric authentication is not available"); errorMessage = "biometric_not_available".tr(); - break; case "LockedOut": _log.warning("User is locked out of biometric authentication"); errorMessage = "biometric_locked_out".tr(); - break; default: _log.warning("Failed to authenticate with unknown reason"); errorMessage = 'failed_to_authenticate'.tr(); diff --git a/mobile/lib/providers/map/map_marker.provider.dart b/mobile/lib/providers/map/map_marker.provider.dart index 38432eab6b..ab73f94c10 100644 --- a/mobile/lib/providers/map/map_marker.provider.dart +++ b/mobile/lib/providers/map/map_marker.provider.dart @@ -8,8 +8,8 @@ final mapMarkersProvider = FutureProvider.autoDispose>((ref) asy final mapState = ref.read(mapStateNotifierProvider); DateTime? fileCreatedAfter; bool? isFavorite; - bool isIncludeArchived = mapState.includeArchived; - bool isWithPartners = mapState.withPartners; + final bool isIncludeArchived = mapState.includeArchived; + final bool isWithPartners = mapState.withPartners; if (mapState.relativeTime != 0) { fileCreatedAfter = DateTime.now().subtract(Duration(days: mapState.relativeTime)); diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index b643264dca..12e9e59fac 100644 --- a/mobile/lib/providers/map/map_state.provider.dart +++ b/mobile/lib/providers/map/map_state.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; @@ -26,12 +28,12 @@ class MapStateNotifier extends Notifier { } void switchTheme(ThemeMode mode) { - ref.read(settingsProvider).write(.mapThemeMode, mode); + unawaited(ref.read(settingsProvider).write(.mapThemeMode, mode)); state = state.copyWith(themeMode: mode); } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); + unawaited(ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly)); state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true); } @@ -40,17 +42,17 @@ class MapStateNotifier extends Notifier { } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); + unawaited(ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived)); state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true); } void switchWithPartners(bool isWithPartners) { - ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); + unawaited(ref.read(settingsProvider).write(.mapWithPartners, isWithPartners)); state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true); } void setRelativeTime(int relativeTime) { - ref.read(settingsProvider).write(.mapRelativeDate, relativeTime); + unawaited(ref.read(settingsProvider).write(.mapRelativeDate, relativeTime)); state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true); } } diff --git a/mobile/lib/providers/oauth.provider.dart b/mobile/lib/providers/oauth.provider.dart index 14b3353943..fd39e0907d 100644 --- a/mobile/lib/providers/oauth.provider.dart +++ b/mobile/lib/providers/oauth.provider.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/oauth.service.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/services/oauth.service.dart'; final oAuthServiceProvider = Provider((ref) => OAuthService(ref.watch(apiServiceProvider))); diff --git a/mobile/lib/providers/permission.provider.dart b/mobile/lib/providers/permission.provider.dart index b7011e1357..bd4dc7b61e 100644 --- a/mobile/lib/providers/permission.provider.dart +++ b/mobile/lib/providers/permission.provider.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use-ref-and-state-synchronously + import 'dart:async'; import 'dart:io'; @@ -10,7 +12,7 @@ class NotificationPermissionNotifier extends StateNotifier { NotificationPermissionNotifier() : super(Platform.isAndroid ? PermissionStatus.granted : PermissionStatus.restricted) { // Sets the initial state - getNotificationPermission().then((p) => state = p); + unawaited(getNotificationPermission().then((p) => state = p)); } /// Requests the notification permission diff --git a/mobile/lib/providers/routes.provider.dart b/mobile/lib/providers/routes.provider.dart index 6cc3d08c5d..776389b330 100644 --- a/mobile/lib/providers/routes.provider.dart +++ b/mobile/lib/providers/routes.provider.dart @@ -5,9 +5,6 @@ import 'package:immich_mobile/routing/router.dart'; @visibleForTesting bool isRouteInStack(Ref ref, String routeName) { final router = ref.watch(appRouterProvider); - void onChange() => ref.invalidateSelf(); - router.addListener(onChange); - ref.onDispose(() => router.removeListener(onChange)); return router.stackData.any((route) => route.name == routeName); } diff --git a/mobile/lib/providers/search/search_input_focus.provider.dart b/mobile/lib/providers/search/search_input_focus.provider.dart index 4f6ed41ee0..4ebbfc6553 100644 --- a/mobile/lib/providers/search/search_input_focus.provider.dart +++ b/mobile/lib/providers/search/search_input_focus.provider.dart @@ -2,5 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final searchInputFocusProvider = Provider((ref) { - return FocusNode(); + final focusNode = FocusNode(); + ref.onDispose(focusNode.dispose); + return focusNode; }); diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 98300894f9..79593cce95 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; @@ -56,11 +58,11 @@ class ServerInfoNotifier extends StateNotifier { } } - _checkServerVersionMismatch(ServerVersion serverVersion, {ServerVersion? latestVersion}) async { - state = state.copyWith(serverVersion: serverVersion, latestVersion: latestVersion); + Future _checkServerVersionMismatch(ServerVersion serverVersion, {ServerVersion? latestVersion}) async { + state = state.copyWith(serverVersion: serverVersion, latestVersion: latestVersion ?? state.latestVersion); - var packageInfo = await PackageInfo.fromPlatform(); - SemVer clientVersion = SemVer.fromString(packageInfo.version); + final packageInfo = await PackageInfo.fromPlatform(); + final SemVer clientVersion = SemVer.fromString(packageInfo.version); if (serverVersion < clientVersion || (latestVersion != null && serverVersion < latestVersion)) { state = state.copyWith(versionStatus: VersionStatus.serverOutOfDate); @@ -75,12 +77,12 @@ class ServerInfoNotifier extends StateNotifier { state = state.copyWith(versionStatus: VersionStatus.upToDate); } - handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) { + void handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) { // Update local server version - _checkServerVersionMismatch(serverVersion, latestVersion: latestVersion); + unawaited(_checkServerVersionMismatch(serverVersion, latestVersion: latestVersion)); } - getServerFeatures() async { + Future getServerFeatures() async { final serverFeatures = await _serverInfoService.getServerFeatures(); if (serverFeatures == null) { return; @@ -88,7 +90,7 @@ class ServerInfoNotifier extends StateNotifier { state = state.copyWith(serverFeatures: serverFeatures); } - getServerConfig() async { + Future getServerConfig() async { final serverConfig = await _serverInfoService.getServerConfig(); if (serverConfig == null) { return; diff --git a/mobile/lib/providers/shared_link.provider.dart b/mobile/lib/providers/shared_link.provider.dart index fb44aea203..096919f28a 100644 --- a/mobile/lib/providers/shared_link.provider.dart +++ b/mobile/lib/providers/shared_link.provider.dart @@ -8,7 +8,7 @@ class SharedLinksNotifier extends StateNotifier>> { final SharedLinkService _sharedLinkService; SharedLinksNotifier(this._sharedLinkService) : super(const AsyncLoading()) { - fetchLinks(); + unawaited(fetchLinks()); } Future fetchLinks() async { diff --git a/mobile/lib/providers/sync_status.provider.dart b/mobile/lib/providers/sync_status.provider.dart index 8d7266abf7..7a70514ca7 100644 --- a/mobile/lib/providers/sync_status.provider.dart +++ b/mobile/lib/providers/sync_status.provider.dart @@ -1,13 +1,17 @@ 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'; +part 'sync_status.provider.freezed.dart'; + enum SyncStatus { idle, syncing, success, error; - localized() { + String localized() { return switch (this) { SyncStatus.idle => "idle".tr(), SyncStatus.syncing => "running".tr(), @@ -17,58 +21,22 @@ enum SyncStatus { } } -class SyncStatusState { - final SyncStatus remoteSyncStatus; - final SyncStatus localSyncStatus; - final SyncStatus hashJobStatus; - final SyncStatus cloudIdSyncStatus; +@freezed +abstract class SyncStatusState with _$SyncStatusState { + const SyncStatusState._(); - final String? errorMessage; - - const SyncStatusState({ - this.remoteSyncStatus = SyncStatus.idle, - this.localSyncStatus = SyncStatus.idle, - this.hashJobStatus = SyncStatus.idle, - this.cloudIdSyncStatus = SyncStatus.idle, - this.errorMessage, - }); - - SyncStatusState copyWith({ - SyncStatus? remoteSyncStatus, - SyncStatus? localSyncStatus, - SyncStatus? hashJobStatus, - SyncStatus? cloudIdSyncStatus, + const factory SyncStatusState({ + @Default(SyncStatus.idle) SyncStatus remoteSyncStatus, + @Default(SyncStatus.idle) SyncStatus localSyncStatus, + @Default(SyncStatus.idle) SyncStatus hashJobStatus, + @Default(SyncStatus.idle) SyncStatus cloudIdSyncStatus, String? errorMessage, - }) { - return SyncStatusState( - remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus, - localSyncStatus: localSyncStatus ?? this.localSyncStatus, - hashJobStatus: hashJobStatus ?? this.hashJobStatus, - cloudIdSyncStatus: cloudIdSyncStatus ?? this.cloudIdSyncStatus, - errorMessage: errorMessage ?? this.errorMessage, - ); - } + }) = _SyncStatusState; bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing; bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing; bool get isHashing => hashJobStatus == SyncStatus.syncing; bool get isCloudIdSyncing => cloudIdSyncStatus == SyncStatus.syncing; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - return other is SyncStatusState && - other.remoteSyncStatus == remoteSyncStatus && - other.localSyncStatus == localSyncStatus && - other.hashJobStatus == hashJobStatus && - other.cloudIdSyncStatus == cloudIdSyncStatus && - other.errorMessage == errorMessage; - } - - @override - int get hashCode => Object.hash(remoteSyncStatus, localSyncStatus, hashJobStatus, cloudIdSyncStatus, errorMessage); } class SyncStatusNotifier extends Notifier { @@ -88,7 +56,11 @@ class SyncStatusNotifier extends Notifier { /// void setRemoteSyncStatus(SyncStatus status, [String? errorMessage]) { - state = state.copyWith(remoteSyncStatus: status, errorMessage: status == SyncStatus.error ? errorMessage : null); + // TODO(agg23): These error messages probably should be cleared, not preserved on null + state = state.copyWith( + remoteSyncStatus: status, + errorMessage: (status == SyncStatus.error ? errorMessage : null) ?? state.errorMessage, + ); } void startRemoteSync() => setRemoteSyncStatus(SyncStatus.syncing); @@ -100,7 +72,11 @@ class SyncStatusNotifier extends Notifier { /// void setLocalSyncStatus(SyncStatus status, [String? errorMessage]) { - state = state.copyWith(localSyncStatus: status, errorMessage: status == SyncStatus.error ? errorMessage : null); + // TODO(agg23): These error messages probably should be cleared, not preserved on null + state = state.copyWith( + localSyncStatus: status, + errorMessage: (status == SyncStatus.error ? errorMessage : null) ?? state.errorMessage, + ); } void startLocalSync() => setLocalSyncStatus(SyncStatus.syncing); @@ -112,7 +88,11 @@ class SyncStatusNotifier extends Notifier { /// void setHashJobStatus(SyncStatus status, [String? errorMessage]) { - state = state.copyWith(hashJobStatus: status, errorMessage: status == SyncStatus.error ? errorMessage : null); + // TODO(agg23): These error messages probably should be cleared, not preserved on null + state = state.copyWith( + hashJobStatus: status, + errorMessage: (status == SyncStatus.error ? errorMessage : null) ?? state.errorMessage, + ); } void startHashJob() => setHashJobStatus(SyncStatus.syncing); @@ -124,7 +104,11 @@ class SyncStatusNotifier extends Notifier { /// void setCloudIdSyncStatus(SyncStatus status, [String? errorMessage]) { - state = state.copyWith(cloudIdSyncStatus: status, errorMessage: status == SyncStatus.error ? errorMessage : null); + // TODO(agg23): These error messages probably should be cleared, not preserved on null + state = state.copyWith( + cloudIdSyncStatus: status, + errorMessage: (status == SyncStatus.error ? errorMessage : null) ?? state.errorMessage, + ); } void startCloudIdSync() => setCloudIdSyncStatus(SyncStatus.syncing); diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index 10c8bb86b6..36de8d5e4f 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use-ref-and-state-synchronously + import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -22,8 +24,6 @@ class MultiSelectState { bool get hasRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged); - bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null); - bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged); bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); @@ -102,7 +102,7 @@ class MultiSelectNotifier extends Notifier { } /// Bucket bulk operations - void selectBucket(int offset, int bucketCount) async { + Future selectBucket(int offset, int bucketCount) async { final assets = await _timelineService.loadAssets(offset, bucketCount); final selectedAssets = state.selectedAssets.toSet(); @@ -111,7 +111,7 @@ class MultiSelectNotifier extends Notifier { state = state.copyWith(selectedAssets: selectedAssets); } - void deselectBucket(int offset, int bucketCount) async { + Future deselectBucket(int offset, int bucketCount) async { final assets = await _timelineService.loadAssets(offset, bucketCount); final selectedAssets = state.selectedAssets.toSet(); @@ -120,7 +120,7 @@ class MultiSelectNotifier extends Notifier { state = state.copyWith(selectedAssets: selectedAssets); } - void toggleBucketSelection(int offset, int bucketCount) async { + Future toggleBucketSelection(int offset, int bucketCount) async { final assets = await _timelineService.loadAssets(offset, bucketCount); toggleBucketSelectionByAssets(assets); } diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index 77772b0205..5e7f7fc732 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -66,7 +66,7 @@ class UploadProfileImageNotifier extends StateNotifier Future upload(XFile file, {String? fileName}) async { state = state.copyWith(status: UploadProfileStatus.loading); - var profileImagePath = await _userService.createProfileImage(fileName ?? file.name, await file.readAsBytes()); + final profileImagePath = await _userService.createProfileImage(fileName ?? file.name, await file.readAsBytes()); if (profileImagePath != null) { dPrint(() => "Successfully upload profile image"); @@ -80,5 +80,5 @@ class UploadProfileImageNotifier extends StateNotifier } final uploadProfileImageProvider = StateNotifierProvider( - ((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))), + (ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider)), ); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 5a56b65793..427a1bf1c7 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -14,7 +14,7 @@ class CurrentUserProvider extends StateNotifier { final UserService _userService; late final StreamSubscription streamSub; - refresh() async { + Future refresh() async { try { await _userService.refreshMyUser(); } catch (_) {} @@ -22,7 +22,7 @@ class CurrentUserProvider extends StateNotifier { @override void dispose() { - streamSub.cancel(); + unawaited(streamSub.cancel()); super.dispose(); } } @@ -30,3 +30,11 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); + +final authUserProvider = Provider((ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access this provider'); + } + return user; +}); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index fa05f9a4cd..32531c354b 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -15,30 +17,11 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; -class WebsocketState { - final Socket? socket; - final bool isConnected; +part 'websocket.provider.freezed.dart'; - const WebsocketState({this.socket, required this.isConnected}); - - WebsocketState copyWith({Socket? socket, bool? isConnected}) { - return WebsocketState(socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected); - } - - @override - String toString() => 'WebsocketState(socket: $socket, isConnected: $isConnected)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is WebsocketState && other.socket == socket && other.isConnected == isConnected; - } - - @override - int get hashCode => socket.hashCode ^ isConnected.hashCode; +@freezed +abstract class WebsocketState with _$WebsocketState { + const factory WebsocketState({Socket? socket, required bool isConnected}) = _WebsocketState; } class WebsocketNotifier extends StateNotifier { @@ -56,14 +39,16 @@ class WebsocketNotifier extends StateNotifier { @override void dispose() { _batchDebouncer.dispose(); + state.socket?.dispose(); super.dispose(); } - /// Connects websocket to server unless already connected + /// Connects websocket to server unless an active socket already exists void connect() { - if (state.isConnected) { + if (state.socket?.active == true) { return; } + state.socket?.dispose(); final authenticationState = _ref.read(authProvider); if (authenticationState.isAuthenticated) { @@ -71,7 +56,7 @@ class WebsocketNotifier extends StateNotifier { final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint)); dPrint(() => "Attempting to connect to websocket"); // Configure socket transports must be specified - Socket socket = io( + final Socket socket = io( endpoint.origin, OptionBuilder() .setPath("${endpoint.path}/socket.io") @@ -84,6 +69,8 @@ class WebsocketNotifier extends StateNotifier { .build(), ); + state = WebsocketState(isConnected: false, socket: socket); + socket.onConnect((_) { dPrint(() => "Established Websocket Connection"); state = WebsocketState(isConnected: true, socket: socket); @@ -91,23 +78,29 @@ class WebsocketNotifier extends StateNotifier { socket.onDisconnect((_) { dPrint(() => "Disconnect to Websocket Connection"); - state = const WebsocketState(isConnected: false, socket: null); + state = WebsocketState(isConnected: false, socket: socket); }); socket.on('error', (errorMessage) { _log.severe("Websocket Error - $errorMessage"); - state = const WebsocketState(isConnected: false, socket: null); + state = WebsocketState(isConnected: false, socket: socket); }); socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReadyV1); socket.on('AssetUploadReadyV2', _handleSyncAssetUploadReadyV2); socket.on('AssetEditReadyV1', _handleSyncAssetEditReadyV1); socket.on('AssetEditReadyV2', _handleSyncAssetEditReadyV2); - socket.on('on_album_update', _handleAlbumUpdate); + socket.on('on_album_update', _handleRemoteChange); + socket.on('on_asset_stack_update', _handleRemoteChange); + socket.on('on_asset_delete', _handleRemoteChange); + socket.on('on_asset_trash', _handleRemoteChange); + socket.on('on_asset_restore', _handleRemoteChange); + socket.on('on_asset_hidden', _handleRemoteChange); + socket.on('on_asset_update', _handleRemoteChange); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { - dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); + dPrint(() => "[WEBSOCKET] Catch Websocket Error - $e"); } } } @@ -143,11 +136,11 @@ class WebsocketNotifier extends StateNotifier { } void _handleOnConfigUpdate(dynamic _) { - _ref.read(serverInfoProvider.notifier).getServerFeatures(); - _ref.read(serverInfoProvider.notifier).getServerConfig(); + unawaited(_ref.read(serverInfoProvider.notifier).getServerFeatures()); + unawaited(_ref.read(serverInfoProvider.notifier).getServerConfig()); } - _handleReleaseUpdates(dynamic data) { + void _handleReleaseUpdates(dynamic data) { // Json guard if (data is! Map) { return; @@ -185,8 +178,8 @@ class WebsocketNotifier extends StateNotifier { unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data)); } - void _handleAlbumUpdate(dynamic _) { - unawaited(_ref.read(backgroundSyncProvider).syncRemote()); + void _handleRemoteChange(dynamic _) { + unawaited(_ref.read(backgroundSyncProvider).syncRemote(enqueue: true)); } void _handleSyncAssetEditReadyV2(dynamic data) { @@ -203,7 +196,7 @@ class WebsocketNotifier extends StateNotifier { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { if (isSyncAlbumEnabled) { - _ref.read(backgroundSyncProvider).syncLinkedAlbum(); + unawaited(_ref.read(backgroundSyncProvider).syncLinkedAlbum()); } }), ); @@ -224,7 +217,7 @@ class WebsocketNotifier extends StateNotifier { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) { if (isSyncAlbumEnabled) { - _ref.read(backgroundSyncProvider).syncLinkedAlbum(); + unawaited(_ref.read(backgroundSyncProvider).syncLinkedAlbum()); } }), ); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 2024b75c6e..9b92b491bf 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -111,11 +111,6 @@ class AssetApiRepository extends ApiRepository { ); } - // TODO(shenlong): remove after action migration - Future updateFavorite(List ids, bool isFavorite) async { - return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: Optional.present(isFavorite))); - } - Future updateLocation(List ids, LatLng location) async { return _api.updateAssets( AssetBulkUpdateDto( diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 4c9b6f6009..6058883544 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -37,9 +37,9 @@ class AssetMediaRepository { Future _androidSupportsTrash() async { if (Platform.isAndroid) { - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - int sdkVersion = androidInfo.version.sdkInt; + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + final int sdkVersion = androidInfo.version.sdkInt; return sdkVersion >= 31; } return false; @@ -324,7 +324,7 @@ class AssetMediaRepository { return 0; } - if (_isCancelled(cancelCompleter)) { + if (_isCancelled(cancelCompleter) || !context.mounted) { await _cleanupTempFiles(tempFiles); return 0; } diff --git a/mobile/lib/repositories/auth_api.repository.dart b/mobile/lib/repositories/auth_api.repository.dart index 05dc7f103a..447826b9aa 100644 --- a/mobile/lib/repositories/auth_api.repository.dart +++ b/mobile/lib/repositories/auth_api.repository.dart @@ -32,7 +32,7 @@ class AuthApiRepository extends ApiRepository { await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7)); } - _mapLoginReponse(LoginResponseDto dto) { + LoginResponse _mapLoginReponse(LoginResponseDto dto) { return LoginResponse( accessToken: dto.accessToken, isAdmin: dto.isAdmin, diff --git a/mobile/lib/repositories/download.repository.dart b/mobile/lib/repositories/download.repository.dart index ed1ffd9e3d..855b8302a9 100644 --- a/mobile/lib/repositories/download.repository.dart +++ b/mobile/lib/repositories/download.repository.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +// ignore: dispose-provided-instances final downloadRepositoryProvider = Provider((ref) => DownloadRepository()); class DownloadRepository { diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index 3a654b7511..8c21af6599 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -25,7 +25,9 @@ class DriftAlbumApiRepository extends ApiRepository { _api.createAlbum( CreateAlbumDto( albumName: name, - description: description == null ? const Optional.absent() : Optional.present(description), + description: description == null + ? const Optional.absent() + : Optional.present(description.isEmpty ? null : description), assetIds: Optional.present(assetIds.toList()), ), ), @@ -36,7 +38,8 @@ class DriftAlbumApiRepository extends ApiRepository { Future<({List removed, List failed})> removeAssets(String albumId, Iterable assetIds) async { final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List removed = [], failed = []; + final List removed = []; + final List failed = []; for (final dto in response) { if (dto.success) { removed.add(dto.id); @@ -55,7 +58,8 @@ class DriftAlbumApiRepository extends ApiRepository { final response = await checkNull( _api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()), abortTrigger: abortTrigger), ); - final List added = [], failed = []; + final List added = []; + final List failed = []; for (final dto in response) { if (dto.success) { added.add(dto.id); @@ -86,7 +90,9 @@ class DriftAlbumApiRepository extends ApiRepository { albumId, UpdateAlbumDto( albumName: name == null ? const Optional.absent() : Optional.present(name), - description: description == null ? const Optional.absent() : Optional.present(description), + description: description == null + ? const Optional.absent() + : Optional.present(description.isEmpty ? null : description), albumThumbnailAssetId: thumbnailAssetId == null ? const Optional.absent() : Optional.present(thumbnailAssetId), diff --git a/mobile/lib/repositories/gcast.repository.dart b/mobile/lib/repositories/gcast.repository.dart index db3e0f45d0..b8ffe79b04 100644 --- a/mobile/lib/repositories/gcast.repository.dart +++ b/mobile/lib/repositories/gcast.repository.dart @@ -1,7 +1,7 @@ import 'package:cast/device.dart'; +import 'package:cast/discovery_service.dart'; import 'package:cast/session.dart'; import 'package:cast/session_manager.dart'; -import 'package:cast/discovery_service.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final gCastRepositoryProvider = Provider((_) { diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 68522490d8..1ebc9825f4 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -4,13 +4,13 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:logging/logging.dart'; -import 'package:http/http.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:logging/logging.dart'; final uploadRepositoryProvider = Provider((ref) => UploadRepository()); @@ -144,7 +144,7 @@ class UploadRepository { logger.warning("Upload $logContext was cancelled"); return UploadResult.cancelled(); } catch (error, stackTrace) { - logger.warning("Error uploading $logContext: ${error.toString()}: $stackTrace"); + logger.warning("Error uploading $logContext: $error: $stackTrace"); return UploadResult.error(errorMessage: error.toString()); } } diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index 57304af44f..a7f6d532e9 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -13,10 +13,20 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { - Future(() { - ref.read(currentRouteNameProvider.notifier).state = route.settings.name; - ref.read(previousRouteNameProvider.notifier).state = previousRoute?.settings.name; - ref.read(previousRouteDataProvider.notifier).state = previousRoute?.settings; - }); + ref.invalidate(inLockedViewProvider); + ref.invalidate(isAssetViewerOpenProvider); + unawaited( + Future(() { + ref.read(currentRouteNameProvider.notifier).state = route.settings.name; + ref.read(previousRouteNameProvider.notifier).state = previousRoute?.settings.name; + ref.read(previousRouteDataProvider.notifier).state = previousRoute?.settings; + }), + ); + } + + @override + void didPop(Route route, Route? previousRoute) { + ref.invalidate(inLockedViewProvider); + ref.invalidate(isAssetViewerOpenProvider); } } diff --git a/mobile/lib/routing/duplicate_guard.dart b/mobile/lib/routing/duplicate_guard.dart index c55c7318d0..bffe6064ee 100644 --- a/mobile/lib/routing/duplicate_guard.dart +++ b/mobile/lib/routing/duplicate_guard.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/utils/debug_print.dart'; class DuplicateGuard extends AutoRouteGuard { const DuplicateGuard(); @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { + Future onNavigation(NavigationResolver resolver, StackRouter router) async { // Duplicate navigation if (resolver.route.name == router.current.name) { dPrint(() => 'DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}'); diff --git a/mobile/lib/routing/locked_guard.dart b/mobile/lib/routing/locked_guard.dart index 38484538e0..da0a025bca 100644 --- a/mobile/lib/routing/locked_guard.dart +++ b/mobile/lib/routing/locked_guard.dart @@ -21,7 +21,7 @@ class LockedGuard extends AutoRouteGuard { LockedGuard(this._apiService, this._secureStorageService, this._localAuth); @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { + Future onNavigation(NavigationResolver resolver, StackRouter router) async { final authStatus = await _apiService.authenticationApi.getAuthStatus(); if (authStatus == null) { @@ -62,13 +62,10 @@ class LockedGuard extends AutoRouteGuard { switch (error.code) { case auth_error.notAvailable: _log.severe("notAvailable: $error"); - break; case auth_error.notEnrolled: _log.severe("not enrolled"); - break; default: _log.severe("error"); - break; } resolver.next(false); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 34d29be945..a4c5bab4fb 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -38,7 +38,6 @@ import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; -import 'package:immich_mobile/presentation/pages/feature_message/whats_new.page.dart'; import 'package:immich_mobile/presentation/pages/download_info.page.dart'; import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; @@ -66,6 +65,7 @@ import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; +import 'package:immich_mobile/presentation/pages/feature_message/whats_new.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; @@ -84,6 +84,7 @@ import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; final appRouterProvider = Provider( + // ignore: dispose-provided-instances (ref) => AppRouter( ref.watch(apiServiceProvider), ref.watch(authServiceProvider), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart deleted file mode 100644 index 1876a784dc..0000000000 --- a/mobile/lib/routing/router.gr.dart +++ /dev/null @@ -1,1890 +0,0 @@ -// dart format width=80 -// GENERATED CODE - DO NOT MODIFY BY HAND - -// ************************************************************************** -// AutoRouterGenerator -// ************************************************************************** - -// ignore_for_file: type=lint -// coverage:ignore-file - -part of 'router.dart'; - -/// generated route for -/// [AppLogDetailPage] -class AppLogDetailRoute extends PageRouteInfo { - AppLogDetailRoute({ - Key? key, - required LogMessage logMessage, - List? children, - }) : super( - AppLogDetailRoute.name, - args: AppLogDetailRouteArgs(key: key, logMessage: logMessage), - initialChildren: children, - ); - - static const String name = 'AppLogDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AppLogDetailPage(key: args.key, logMessage: args.logMessage); - }, - ); -} - -class AppLogDetailRouteArgs { - const AppLogDetailRouteArgs({this.key, required this.logMessage}); - - final Key? key; - - final LogMessage logMessage; - - @override - String toString() { - return 'AppLogDetailRouteArgs{key: $key, logMessage: $logMessage}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! AppLogDetailRouteArgs) return false; - return key == other.key && logMessage == other.logMessage; - } - - @override - int get hashCode => key.hashCode ^ logMessage.hashCode; -} - -/// generated route for -/// [AppLogPage] -class AppLogRoute extends PageRouteInfo { - const AppLogRoute({List? children}) - : super(AppLogRoute.name, initialChildren: children); - - static const String name = 'AppLogRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AppLogPage(); - }, - ); -} - -/// generated route for -/// [AssetTroubleshootPage] -class AssetTroubleshootRoute extends PageRouteInfo { - AssetTroubleshootRoute({ - Key? key, - required BaseAsset asset, - List? children, - }) : super( - AssetTroubleshootRoute.name, - args: AssetTroubleshootRouteArgs(key: key, asset: asset), - initialChildren: children, - ); - - static const String name = 'AssetTroubleshootRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AssetTroubleshootPage(key: args.key, asset: args.asset); - }, - ); -} - -class AssetTroubleshootRouteArgs { - const AssetTroubleshootRouteArgs({this.key, required this.asset}); - - final Key? key; - - final BaseAsset asset; - - @override - String toString() { - return 'AssetTroubleshootRouteArgs{key: $key, asset: $asset}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! AssetTroubleshootRouteArgs) return false; - return key == other.key && asset == other.asset; - } - - @override - int get hashCode => key.hashCode ^ asset.hashCode; -} - -/// generated route for -/// [AssetViewerPage] -class AssetViewerRoute extends PageRouteInfo { - AssetViewerRoute({ - Key? key, - required int initialIndex, - required TimelineService timelineService, - int? heroOffset, - RemoteAlbum? currentAlbum, - List? children, - }) : super( - AssetViewerRoute.name, - args: AssetViewerRouteArgs( - key: key, - initialIndex: initialIndex, - timelineService: timelineService, - heroOffset: heroOffset, - currentAlbum: currentAlbum, - ), - initialChildren: children, - ); - - static const String name = 'AssetViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AssetViewerPage( - key: args.key, - initialIndex: args.initialIndex, - timelineService: args.timelineService, - heroOffset: args.heroOffset, - currentAlbum: args.currentAlbum, - ); - }, - ); -} - -class AssetViewerRouteArgs { - const AssetViewerRouteArgs({ - this.key, - required this.initialIndex, - required this.timelineService, - this.heroOffset, - this.currentAlbum, - }); - - final Key? key; - - final int initialIndex; - - final TimelineService timelineService; - - final int? heroOffset; - - final RemoteAlbum? currentAlbum; - - @override - String toString() { - return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService, heroOffset: $heroOffset, currentAlbum: $currentAlbum}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! AssetViewerRouteArgs) return false; - return key == other.key && - initialIndex == other.initialIndex && - timelineService == other.timelineService && - heroOffset == other.heroOffset && - currentAlbum == other.currentAlbum; - } - - @override - int get hashCode => - key.hashCode ^ - initialIndex.hashCode ^ - timelineService.hashCode ^ - heroOffset.hashCode ^ - currentAlbum.hashCode; -} - -/// generated route for -/// [ChangePasswordPage] -class ChangePasswordRoute extends PageRouteInfo { - const ChangePasswordRoute({List? children}) - : super(ChangePasswordRoute.name, initialChildren: children); - - static const String name = 'ChangePasswordRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ChangePasswordPage(); - }, - ); -} - -/// generated route for -/// [CleanupPreviewPage] -class CleanupPreviewRoute extends PageRouteInfo { - CleanupPreviewRoute({ - Key? key, - required List assets, - List? children, - }) : super( - CleanupPreviewRoute.name, - args: CleanupPreviewRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'CleanupPreviewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return CleanupPreviewPage(key: args.key, assets: args.assets); - }, - ); -} - -class CleanupPreviewRouteArgs { - const CleanupPreviewRouteArgs({this.key, required this.assets}); - - final Key? key; - - final List assets; - - @override - String toString() { - return 'CleanupPreviewRouteArgs{key: $key, assets: $assets}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! CleanupPreviewRouteArgs) return false; - return key == other.key && - const ListEquality().equals(assets, other.assets); - } - - @override - int get hashCode => - key.hashCode ^ const ListEquality().hash(assets); -} - -/// generated route for -/// [DownloadInfoPage] -class DownloadInfoRoute extends PageRouteInfo { - const DownloadInfoRoute({List? children}) - : super(DownloadInfoRoute.name, initialChildren: children); - - static const String name = 'DownloadInfoRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DownloadInfoPage(); - }, - ); -} - -/// generated route for -/// [DriftActivitiesPage] -class DriftActivitiesRoute extends PageRouteInfo { - DriftActivitiesRoute({ - Key? key, - required RemoteAlbum album, - String? assetId, - String? assetName, - List? children, - }) : super( - DriftActivitiesRoute.name, - args: DriftActivitiesRouteArgs( - key: key, - album: album, - assetId: assetId, - assetName: assetName, - ), - initialChildren: children, - ); - - static const String name = 'DriftActivitiesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftActivitiesPage( - key: args.key, - album: args.album, - assetId: args.assetId, - assetName: args.assetName, - ); - }, - ); -} - -class DriftActivitiesRouteArgs { - const DriftActivitiesRouteArgs({ - this.key, - required this.album, - this.assetId, - this.assetName, - }); - - final Key? key; - - final RemoteAlbum album; - - final String? assetId; - - final String? assetName; - - @override - String toString() { - return 'DriftActivitiesRouteArgs{key: $key, album: $album, assetId: $assetId, assetName: $assetName}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftActivitiesRouteArgs) return false; - return key == other.key && - album == other.album && - assetId == other.assetId && - assetName == other.assetName; - } - - @override - int get hashCode => - key.hashCode ^ album.hashCode ^ assetId.hashCode ^ assetName.hashCode; -} - -/// generated route for -/// [DriftAlbumOptionsPage] -class DriftAlbumOptionsRoute extends PageRouteInfo { - DriftAlbumOptionsRoute({ - Key? key, - required RemoteAlbum album, - List? children, - }) : super( - DriftAlbumOptionsRoute.name, - args: DriftAlbumOptionsRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'DriftAlbumOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftAlbumOptionsPage(key: args.key, album: args.album); - }, - ); -} - -class DriftAlbumOptionsRouteArgs { - const DriftAlbumOptionsRouteArgs({this.key, required this.album}); - - final Key? key; - - final RemoteAlbum album; - - @override - String toString() { - return 'DriftAlbumOptionsRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftAlbumOptionsRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [DriftAlbumsPage] -class DriftAlbumsRoute extends PageRouteInfo { - const DriftAlbumsRoute({List? children}) - : super(DriftAlbumsRoute.name, initialChildren: children); - - static const String name = 'DriftAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftAlbumsPage(); - }, - ); -} - -/// generated route for -/// [DriftArchivePage] -class DriftArchiveRoute extends PageRouteInfo { - const DriftArchiveRoute({List? children}) - : super(DriftArchiveRoute.name, initialChildren: children); - - static const String name = 'DriftArchiveRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftArchivePage(); - }, - ); -} - -/// generated route for -/// [DriftAssetSelectionTimelinePage] -class DriftAssetSelectionTimelineRoute - extends PageRouteInfo { - DriftAssetSelectionTimelineRoute({ - Key? key, - Set lockedSelectionAssets = const {}, - List? children, - }) : super( - DriftAssetSelectionTimelineRoute.name, - args: DriftAssetSelectionTimelineRouteArgs( - key: key, - lockedSelectionAssets: lockedSelectionAssets, - ), - initialChildren: children, - ); - - static const String name = 'DriftAssetSelectionTimelineRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const DriftAssetSelectionTimelineRouteArgs(), - ); - return DriftAssetSelectionTimelinePage( - key: args.key, - lockedSelectionAssets: args.lockedSelectionAssets, - ); - }, - ); -} - -class DriftAssetSelectionTimelineRouteArgs { - const DriftAssetSelectionTimelineRouteArgs({ - this.key, - this.lockedSelectionAssets = const {}, - }); - - final Key? key; - - final Set lockedSelectionAssets; - - @override - String toString() { - return 'DriftAssetSelectionTimelineRouteArgs{key: $key, lockedSelectionAssets: $lockedSelectionAssets}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftAssetSelectionTimelineRouteArgs) return false; - return key == other.key && - const SetEquality().equals( - lockedSelectionAssets, - other.lockedSelectionAssets, - ); - } - - @override - int get hashCode => - key.hashCode ^ const SetEquality().hash(lockedSelectionAssets); -} - -/// generated route for -/// [DriftBackupAlbumSelectionPage] -class DriftBackupAlbumSelectionRoute extends PageRouteInfo { - const DriftBackupAlbumSelectionRoute({List? children}) - : super(DriftBackupAlbumSelectionRoute.name, initialChildren: children); - - static const String name = 'DriftBackupAlbumSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupAlbumSelectionPage(); - }, - ); -} - -/// generated route for -/// [DriftBackupAssetDetailPage] -class DriftBackupAssetDetailRoute extends PageRouteInfo { - const DriftBackupAssetDetailRoute({List? children}) - : super(DriftBackupAssetDetailRoute.name, initialChildren: children); - - static const String name = 'DriftBackupAssetDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupAssetDetailPage(); - }, - ); -} - -/// generated route for -/// [DriftBackupOptionsPage] -class DriftBackupOptionsRoute extends PageRouteInfo { - const DriftBackupOptionsRoute({List? children}) - : super(DriftBackupOptionsRoute.name, initialChildren: children); - - static const String name = 'DriftBackupOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupOptionsPage(); - }, - ); -} - -/// generated route for -/// [DriftBackupPage] -class DriftBackupRoute extends PageRouteInfo { - const DriftBackupRoute({List? children}) - : super(DriftBackupRoute.name, initialChildren: children); - - static const String name = 'DriftBackupRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupPage(); - }, - ); -} - -/// generated route for -/// [DriftCreateAlbumPage] -class DriftCreateAlbumRoute extends PageRouteInfo { - const DriftCreateAlbumRoute({List? children}) - : super(DriftCreateAlbumRoute.name, initialChildren: children); - - static const String name = 'DriftCreateAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftCreateAlbumPage(); - }, - ); -} - -/// generated route for -/// [DriftEditImagePage] -class DriftEditImageRoute extends PageRouteInfo { - DriftEditImageRoute({ - Key? key, - required Image image, - required Future Function(List) applyEdits, - List? children, - }) : super( - DriftEditImageRoute.name, - args: DriftEditImageRouteArgs( - key: key, - image: image, - applyEdits: applyEdits, - ), - initialChildren: children, - ); - - static const String name = 'DriftEditImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftEditImagePage( - key: args.key, - image: args.image, - applyEdits: args.applyEdits, - ); - }, - ); -} - -class DriftEditImageRouteArgs { - const DriftEditImageRouteArgs({ - this.key, - required this.image, - required this.applyEdits, - }); - - final Key? key; - - final Image image; - - final Future Function(List) applyEdits; - - @override - String toString() { - return 'DriftEditImageRouteArgs{key: $key, image: $image, applyEdits: $applyEdits}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftEditImageRouteArgs) return false; - return key == other.key && image == other.image; - } - - @override - int get hashCode => key.hashCode ^ image.hashCode; -} - -/// generated route for -/// [DriftFavoritePage] -class DriftFavoriteRoute extends PageRouteInfo { - const DriftFavoriteRoute({List? children}) - : super(DriftFavoriteRoute.name, initialChildren: children); - - static const String name = 'DriftFavoriteRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftFavoritePage(); - }, - ); -} - -/// generated route for -/// [DriftLibraryPage] -class DriftLibraryRoute extends PageRouteInfo { - const DriftLibraryRoute({List? children}) - : super(DriftLibraryRoute.name, initialChildren: children); - - static const String name = 'DriftLibraryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftLibraryPage(); - }, - ); -} - -/// generated route for -/// [DriftLocalAlbumsPage] -class DriftLocalAlbumsRoute extends PageRouteInfo { - const DriftLocalAlbumsRoute({List? children}) - : super(DriftLocalAlbumsRoute.name, initialChildren: children); - - static const String name = 'DriftLocalAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftLocalAlbumsPage(); - }, - ); -} - -/// generated route for -/// [DriftLockedFolderPage] -class DriftLockedFolderRoute extends PageRouteInfo { - const DriftLockedFolderRoute({List? children}) - : super(DriftLockedFolderRoute.name, initialChildren: children); - - static const String name = 'DriftLockedFolderRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftLockedFolderPage(); - }, - ); -} - -/// generated route for -/// [DriftMapPage] -class DriftMapRoute extends PageRouteInfo { - DriftMapRoute({ - Key? key, - LatLng? initialLocation, - List? children, - }) : super( - DriftMapRoute.name, - args: DriftMapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); - - static const String name = 'DriftMapRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const DriftMapRouteArgs(), - ); - return DriftMapPage(key: args.key, initialLocation: args.initialLocation); - }, - ); -} - -class DriftMapRouteArgs { - const DriftMapRouteArgs({this.key, this.initialLocation}); - - final Key? key; - - final LatLng? initialLocation; - - @override - String toString() { - return 'DriftMapRouteArgs{key: $key, initialLocation: $initialLocation}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftMapRouteArgs) return false; - return key == other.key && initialLocation == other.initialLocation; - } - - @override - int get hashCode => key.hashCode ^ initialLocation.hashCode; -} - -/// generated route for -/// [DriftMemoryPage] -class DriftMemoryRoute extends PageRouteInfo { - DriftMemoryRoute({ - required List memories, - required int memoryIndex, - Key? key, - List? children, - }) : super( - DriftMemoryRoute.name, - args: DriftMemoryRouteArgs( - memories: memories, - memoryIndex: memoryIndex, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'DriftMemoryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftMemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ); - }, - ); -} - -class DriftMemoryRouteArgs { - const DriftMemoryRouteArgs({ - required this.memories, - required this.memoryIndex, - this.key, - }); - - final List memories; - - final int memoryIndex; - - final Key? key; - - @override - String toString() { - return 'DriftMemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftMemoryRouteArgs) return false; - return const ListEquality().equals(memories, other.memories) && - memoryIndex == other.memoryIndex && - key == other.key; - } - - @override - int get hashCode => - const ListEquality().hash(memories) ^ - memoryIndex.hashCode ^ - key.hashCode; -} - -/// generated route for -/// [DriftPartnerDetailPage] -class DriftPartnerDetailRoute - extends PageRouteInfo { - DriftPartnerDetailRoute({ - Key? key, - required Partner partner, - List? children, - }) : super( - DriftPartnerDetailRoute.name, - args: DriftPartnerDetailRouteArgs(key: key, partner: partner), - initialChildren: children, - ); - - static const String name = 'DriftPartnerDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftPartnerDetailPage(key: args.key, partner: args.partner); - }, - ); -} - -class DriftPartnerDetailRouteArgs { - const DriftPartnerDetailRouteArgs({this.key, required this.partner}); - - final Key? key; - - final Partner partner; - - @override - String toString() { - return 'DriftPartnerDetailRouteArgs{key: $key, partner: $partner}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPartnerDetailRouteArgs) return false; - return key == other.key && partner == other.partner; - } - - @override - int get hashCode => key.hashCode ^ partner.hashCode; -} - -/// generated route for -/// [DriftPeopleCollectionPage] -class DriftPeopleCollectionRoute extends PageRouteInfo { - const DriftPeopleCollectionRoute({List? children}) - : super(DriftPeopleCollectionRoute.name, initialChildren: children); - - static const String name = 'DriftPeopleCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftPeopleCollectionPage(); - }, - ); -} - -/// generated route for -/// [DriftPersonPage] -class DriftPersonRoute extends PageRouteInfo { - DriftPersonRoute({ - Key? key, - required DriftPerson person, - List? children, - }) : super( - DriftPersonRoute.name, - args: DriftPersonRouteArgs(key: key, person: person), - initialChildren: children, - ); - - static const String name = 'DriftPersonRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftPersonPage(key: args.key, person: args.person); - }, - ); -} - -class DriftPersonRouteArgs { - const DriftPersonRouteArgs({this.key, required this.person}); - - final Key? key; - - final DriftPerson person; - - @override - String toString() { - return 'DriftPersonRouteArgs{key: $key, person: $person}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPersonRouteArgs) return false; - return key == other.key && person == other.person; - } - - @override - int get hashCode => key.hashCode ^ person.hashCode; -} - -/// generated route for -/// [DriftPlaceDetailPage] -class DriftPlaceDetailRoute extends PageRouteInfo { - DriftPlaceDetailRoute({ - Key? key, - required String place, - List? children, - }) : super( - DriftPlaceDetailRoute.name, - args: DriftPlaceDetailRouteArgs(key: key, place: place), - initialChildren: children, - ); - - static const String name = 'DriftPlaceDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftPlaceDetailPage(key: args.key, place: args.place); - }, - ); -} - -class DriftPlaceDetailRouteArgs { - const DriftPlaceDetailRouteArgs({this.key, required this.place}); - - final Key? key; - - final String place; - - @override - String toString() { - return 'DriftPlaceDetailRouteArgs{key: $key, place: $place}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPlaceDetailRouteArgs) return false; - return key == other.key && place == other.place; - } - - @override - int get hashCode => key.hashCode ^ place.hashCode; -} - -/// generated route for -/// [DriftPlacePage] -class DriftPlaceRoute extends PageRouteInfo { - DriftPlaceRoute({ - Key? key, - LatLng? currentLocation, - List? children, - }) : super( - DriftPlaceRoute.name, - args: DriftPlaceRouteArgs(key: key, currentLocation: currentLocation), - initialChildren: children, - ); - - static const String name = 'DriftPlaceRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const DriftPlaceRouteArgs(), - ); - return DriftPlacePage( - key: args.key, - currentLocation: args.currentLocation, - ); - }, - ); -} - -class DriftPlaceRouteArgs { - const DriftPlaceRouteArgs({this.key, this.currentLocation}); - - final Key? key; - - final LatLng? currentLocation; - - @override - String toString() { - return 'DriftPlaceRouteArgs{key: $key, currentLocation: $currentLocation}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPlaceRouteArgs) return false; - return key == other.key && currentLocation == other.currentLocation; - } - - @override - int get hashCode => key.hashCode ^ currentLocation.hashCode; -} - -/// generated route for -/// [DriftRecentlyAddedPage] -class DriftRecentlyAddedRoute extends PageRouteInfo { - const DriftRecentlyAddedRoute({List? children}) - : super(DriftRecentlyAddedRoute.name, initialChildren: children); - - static const String name = 'DriftRecentlyAddedRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftRecentlyAddedPage(); - }, - ); -} - -/// generated route for -/// [DriftRecentlyTakenPage] -class DriftRecentlyTakenRoute extends PageRouteInfo { - const DriftRecentlyTakenRoute({List? children}) - : super(DriftRecentlyTakenRoute.name, initialChildren: children); - - static const String name = 'DriftRecentlyTakenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftRecentlyTakenPage(); - }, - ); -} - -/// generated route for -/// [DriftSearchPage] -class DriftSearchRoute extends PageRouteInfo { - const DriftSearchRoute({List? children}) - : super(DriftSearchRoute.name, initialChildren: children); - - static const String name = 'DriftSearchRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftSearchPage(); - }, - ); -} - -/// generated route for -/// [DriftSlideshowPage] -class DriftSlideshowRoute extends PageRouteInfo { - DriftSlideshowRoute({ - Key? key, - required TimelineService timeline, - List? children, - }) : super( - DriftSlideshowRoute.name, - args: DriftSlideshowRouteArgs(key: key, timeline: timeline), - initialChildren: children, - ); - - static const String name = 'DriftSlideshowRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftSlideshowPage(key: args.key, timeline: args.timeline); - }, - ); -} - -class DriftSlideshowRouteArgs { - const DriftSlideshowRouteArgs({this.key, required this.timeline}); - - final Key? key; - - final TimelineService timeline; - - @override - String toString() { - return 'DriftSlideshowRouteArgs{key: $key, timeline: $timeline}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftSlideshowRouteArgs) return false; - return key == other.key && timeline == other.timeline; - } - - @override - int get hashCode => key.hashCode ^ timeline.hashCode; -} - -/// generated route for -/// [DriftTrashPage] -class DriftTrashRoute extends PageRouteInfo { - const DriftTrashRoute({List? children}) - : super(DriftTrashRoute.name, initialChildren: children); - - static const String name = 'DriftTrashRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftTrashPage(); - }, - ); -} - -/// generated route for -/// [DriftUploadDetailPage] -class DriftUploadDetailRoute extends PageRouteInfo { - const DriftUploadDetailRoute({List? children}) - : super(DriftUploadDetailRoute.name, initialChildren: children); - - static const String name = 'DriftUploadDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftUploadDetailPage(); - }, - ); -} - -/// generated route for -/// [DriftUserSelectionPage] -class DriftUserSelectionRoute - extends PageRouteInfo { - DriftUserSelectionRoute({ - Key? key, - required RemoteAlbum album, - List? children, - }) : super( - DriftUserSelectionRoute.name, - args: DriftUserSelectionRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'DriftUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftUserSelectionPage(key: args.key, album: args.album); - }, - ); -} - -class DriftUserSelectionRouteArgs { - const DriftUserSelectionRouteArgs({this.key, required this.album}); - - final Key? key; - - final RemoteAlbum album; - - @override - String toString() { - return 'DriftUserSelectionRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftUserSelectionRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [DriftVideoPage] -class DriftVideoRoute extends PageRouteInfo { - const DriftVideoRoute({List? children}) - : super(DriftVideoRoute.name, initialChildren: children); - - static const String name = 'DriftVideoRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftVideoPage(); - }, - ); -} - -/// generated route for -/// [FolderPage] -class FolderRoute extends PageRouteInfo { - FolderRoute({ - Key? key, - RecursiveFolder? folder, - List? children, - }) : super( - FolderRoute.name, - args: FolderRouteArgs(key: key, folder: folder), - initialChildren: children, - ); - - static const String name = 'FolderRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const FolderRouteArgs(), - ); - return FolderPage(key: args.key, folder: args.folder); - }, - ); -} - -class FolderRouteArgs { - const FolderRouteArgs({this.key, this.folder}); - - final Key? key; - - final RecursiveFolder? folder; - - @override - String toString() { - return 'FolderRouteArgs{key: $key, folder: $folder}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! FolderRouteArgs) return false; - return key == other.key && folder == other.folder; - } - - @override - int get hashCode => key.hashCode ^ folder.hashCode; -} - -/// generated route for -/// [HeaderSettingsPage] -class HeaderSettingsRoute extends PageRouteInfo { - const HeaderSettingsRoute({List? children}) - : super(HeaderSettingsRoute.name, initialChildren: children); - - static const String name = 'HeaderSettingsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const HeaderSettingsPage(); - }, - ); -} - -/// generated route for -/// [LocalMediaSummaryPage] -class LocalMediaSummaryRoute extends PageRouteInfo { - const LocalMediaSummaryRoute({List? children}) - : super(LocalMediaSummaryRoute.name, initialChildren: children); - - static const String name = 'LocalMediaSummaryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LocalMediaSummaryPage(); - }, - ); -} - -/// generated route for -/// [LocalTimelinePage] -class LocalTimelineRoute extends PageRouteInfo { - LocalTimelineRoute({ - Key? key, - required LocalAlbum album, - List? children, - }) : super( - LocalTimelineRoute.name, - args: LocalTimelineRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'LocalTimelineRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return LocalTimelinePage(key: args.key, album: args.album); - }, - ); -} - -class LocalTimelineRouteArgs { - const LocalTimelineRouteArgs({this.key, required this.album}); - - final Key? key; - - final LocalAlbum album; - - @override - String toString() { - return 'LocalTimelineRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! LocalTimelineRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [LoginPage] -class LoginRoute extends PageRouteInfo { - const LoginRoute({List? children}) - : super(LoginRoute.name, initialChildren: children); - - static const String name = 'LoginRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LoginPage(); - }, - ); -} - -/// generated route for -/// [MainTimelinePage] -class MainTimelineRoute extends PageRouteInfo { - const MainTimelineRoute({List? children}) - : super(MainTimelineRoute.name, initialChildren: children); - - static const String name = 'MainTimelineRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const MainTimelinePage(); - }, - ); -} - -/// generated route for -/// [MapLocationPickerPage] -class MapLocationPickerRoute extends PageRouteInfo { - MapLocationPickerRoute({ - Key? key, - LatLng initialLatLng = const LatLng(0, 0), - List? children, - }) : super( - MapLocationPickerRoute.name, - args: MapLocationPickerRouteArgs( - key: key, - initialLatLng: initialLatLng, - ), - initialChildren: children, - ); - - static const String name = 'MapLocationPickerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const MapLocationPickerRouteArgs(), - ); - return MapLocationPickerPage( - key: args.key, - initialLatLng: args.initialLatLng, - ); - }, - ); -} - -class MapLocationPickerRouteArgs { - const MapLocationPickerRouteArgs({ - this.key, - this.initialLatLng = const LatLng(0, 0), - }); - - final Key? key; - - final LatLng initialLatLng; - - @override - String toString() { - return 'MapLocationPickerRouteArgs{key: $key, initialLatLng: $initialLatLng}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! MapLocationPickerRouteArgs) return false; - return key == other.key && initialLatLng == other.initialLatLng; - } - - @override - int get hashCode => key.hashCode ^ initialLatLng.hashCode; -} - -/// generated route for -/// [PartnerPage] -class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); - - static const String name = 'PartnerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PartnerPage(); - }, - ); -} - -/// generated route for -/// [PinAuthPage] -class PinAuthRoute extends PageRouteInfo { - PinAuthRoute({ - Key? key, - bool createPinCode = false, - List? children, - }) : super( - PinAuthRoute.name, - args: PinAuthRouteArgs(key: key, createPinCode: createPinCode), - initialChildren: children, - ); - - static const String name = 'PinAuthRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const PinAuthRouteArgs(), - ); - return PinAuthPage(key: args.key, createPinCode: args.createPinCode); - }, - ); -} - -class PinAuthRouteArgs { - const PinAuthRouteArgs({this.key, this.createPinCode = false}); - - final Key? key; - - final bool createPinCode; - - @override - String toString() { - return 'PinAuthRouteArgs{key: $key, createPinCode: $createPinCode}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! PinAuthRouteArgs) return false; - return key == other.key && createPinCode == other.createPinCode; - } - - @override - int get hashCode => key.hashCode ^ createPinCode.hashCode; -} - -/// generated route for -/// [ProfilePictureCropPage] -class ProfilePictureCropRoute - extends PageRouteInfo { - ProfilePictureCropRoute({ - Key? key, - required BaseAsset asset, - List? children, - }) : super( - ProfilePictureCropRoute.name, - args: ProfilePictureCropRouteArgs(key: key, asset: asset), - initialChildren: children, - ); - - static const String name = 'ProfilePictureCropRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ProfilePictureCropPage(key: args.key, asset: args.asset); - }, - ); -} - -class ProfilePictureCropRouteArgs { - const ProfilePictureCropRouteArgs({this.key, required this.asset}); - - final Key? key; - - final BaseAsset asset; - - @override - String toString() { - return 'ProfilePictureCropRouteArgs{key: $key, asset: $asset}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! ProfilePictureCropRouteArgs) return false; - return key == other.key && asset == other.asset; - } - - @override - int get hashCode => key.hashCode ^ asset.hashCode; -} - -/// generated route for -/// [RemoteAlbumPage] -class RemoteAlbumRoute extends PageRouteInfo { - RemoteAlbumRoute({ - Key? key, - required RemoteAlbum album, - List? children, - }) : super( - RemoteAlbumRoute.name, - args: RemoteAlbumRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'RemoteAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return RemoteAlbumPage(key: args.key, album: args.album); - }, - ); -} - -class RemoteAlbumRouteArgs { - const RemoteAlbumRouteArgs({this.key, required this.album}); - - final Key? key; - - final RemoteAlbum album; - - @override - String toString() { - return 'RemoteAlbumRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! RemoteAlbumRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [RemoteMediaSummaryPage] -class RemoteMediaSummaryRoute extends PageRouteInfo { - const RemoteMediaSummaryRoute({List? children}) - : super(RemoteMediaSummaryRoute.name, initialChildren: children); - - static const String name = 'RemoteMediaSummaryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const RemoteMediaSummaryPage(); - }, - ); -} - -/// generated route for -/// [SettingsPage] -class SettingsRoute extends PageRouteInfo { - const SettingsRoute({List? children}) - : super(SettingsRoute.name, initialChildren: children); - - static const String name = 'SettingsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SettingsPage(); - }, - ); -} - -/// generated route for -/// [SettingsSubPage] -class SettingsSubRoute extends PageRouteInfo { - SettingsSubRoute({ - required SettingSection section, - Key? key, - List? children, - }) : super( - SettingsSubRoute.name, - args: SettingsSubRouteArgs(section: section, key: key), - initialChildren: children, - ); - - static const String name = 'SettingsSubRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return SettingsSubPage(args.section, key: args.key); - }, - ); -} - -class SettingsSubRouteArgs { - const SettingsSubRouteArgs({required this.section, this.key}); - - final SettingSection section; - - final Key? key; - - @override - String toString() { - return 'SettingsSubRouteArgs{section: $section, key: $key}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! SettingsSubRouteArgs) return false; - return section == other.section && key == other.key; - } - - @override - int get hashCode => section.hashCode ^ key.hashCode; -} - -/// generated route for -/// [ShareIntentPage] -class ShareIntentRoute extends PageRouteInfo { - ShareIntentRoute({ - Key? key, - required List attachments, - List? children, - }) : super( - ShareIntentRoute.name, - args: ShareIntentRouteArgs(key: key, attachments: attachments), - initialChildren: children, - ); - - static const String name = 'ShareIntentRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ShareIntentPage(key: args.key, attachments: args.attachments); - }, - ); -} - -class ShareIntentRouteArgs { - const ShareIntentRouteArgs({this.key, required this.attachments}); - - final Key? key; - - final List attachments; - - @override - String toString() { - return 'ShareIntentRouteArgs{key: $key, attachments: $attachments}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! ShareIntentRouteArgs) return false; - return key == other.key && - const ListEquality().equals( - attachments, - other.attachments, - ); - } - - @override - int get hashCode => - key.hashCode ^ - const ListEquality().hash(attachments); -} - -/// generated route for -/// [SharedLinkEditPage] -class SharedLinkEditRoute extends PageRouteInfo { - SharedLinkEditRoute({ - Key? key, - SharedLink? existingLink, - List? assetsList, - String? albumId, - List? children, - }) : super( - SharedLinkEditRoute.name, - args: SharedLinkEditRouteArgs( - key: key, - existingLink: existingLink, - assetsList: assetsList, - albumId: albumId, - ), - initialChildren: children, - ); - - static const String name = 'SharedLinkEditRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const SharedLinkEditRouteArgs(), - ); - return SharedLinkEditPage( - key: args.key, - existingLink: args.existingLink, - assetsList: args.assetsList, - albumId: args.albumId, - ); - }, - ); -} - -class SharedLinkEditRouteArgs { - const SharedLinkEditRouteArgs({ - this.key, - this.existingLink, - this.assetsList, - this.albumId, - }); - - final Key? key; - - final SharedLink? existingLink; - - final List? assetsList; - - final String? albumId; - - @override - String toString() { - return 'SharedLinkEditRouteArgs{key: $key, existingLink: $existingLink, assetsList: $assetsList, albumId: $albumId}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! SharedLinkEditRouteArgs) return false; - return key == other.key && - existingLink == other.existingLink && - const ListEquality().equals(assetsList, other.assetsList) && - albumId == other.albumId; - } - - @override - int get hashCode => - key.hashCode ^ - existingLink.hashCode ^ - const ListEquality().hash(assetsList) ^ - albumId.hashCode; -} - -/// generated route for -/// [SharedLinkPage] -class SharedLinkRoute extends PageRouteInfo { - const SharedLinkRoute({List? children}) - : super(SharedLinkRoute.name, initialChildren: children); - - static const String name = 'SharedLinkRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SharedLinkPage(); - }, - ); -} - -/// generated route for -/// [SplashScreenPage] -class SplashScreenRoute extends PageRouteInfo { - const SplashScreenRoute({List? children}) - : super(SplashScreenRoute.name, initialChildren: children); - - static const String name = 'SplashScreenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SplashScreenPage(); - }, - ); -} - -/// generated route for -/// [SyncStatusPage] -class SyncStatusRoute extends PageRouteInfo { - const SyncStatusRoute({List? children}) - : super(SyncStatusRoute.name, initialChildren: children); - - static const String name = 'SyncStatusRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SyncStatusPage(); - }, - ); -} - -/// generated route for -/// [TabShellPage] -class TabShellRoute extends PageRouteInfo { - const TabShellRoute({List? children}) - : super(TabShellRoute.name, initialChildren: children); - - static const String name = 'TabShellRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TabShellPage(); - }, - ); -} - -/// generated route for -/// [WhatsNewPage] -class WhatsNewRoute extends PageRouteInfo { - const WhatsNewRoute({List? children}) - : super(WhatsNewRoute.name, initialChildren: children); - - static const String name = 'WhatsNewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const WhatsNewPage(); - }, - ); -} diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 19782c8512..ec9b7cd446 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -1,117 +1,19 @@ 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/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/asset_edit.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/tag.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/download.repository.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/timezone.dart'; -import 'package:immich_mobile/widgets/common/date_time_picker.dart'; -import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:immich_mobile/widgets/common/tag_picker.dart'; -import 'package:maplibre_gl/maplibre_gl.dart' as maplibre; final actionServiceProvider = Provider( - (ref) => ActionService( - ref.watch(assetApiRepositoryProvider), - ref.watch(remoteAssetRepositoryProvider), - ref.watch(localAssetRepository), - ref.watch(driftAlbumApiRepositoryProvider), - ref.watch(remoteAlbumRepository), - ref.watch(trashedLocalAssetRepository), - ref.watch(assetMediaRepositoryProvider), - ref.watch(downloadRepositoryProvider), - ref.watch(tagServiceProvider), - ), + (ref) => ActionService(ref.watch(assetApiRepositoryProvider), ref.watch(remoteAssetRepositoryProvider)), ); class ActionService { final AssetApiRepository _assetApiRepository; final RemoteAssetRepository _remoteAssetRepository; - final DriftLocalAssetRepository _localAssetRepository; - final DriftAlbumApiRepository _albumApiRepository; - final DriftRemoteAlbumRepository _remoteAlbumRepository; - final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; - final AssetMediaRepository _assetMediaRepository; - final DownloadRepository _downloadRepository; - final TagService _tagService; - const ActionService( - this._assetApiRepository, - this._remoteAssetRepository, - this._localAssetRepository, - this._albumApiRepository, - this._remoteAlbumRepository, - this._trashedLocalAssetRepository, - this._assetMediaRepository, - this._downloadRepository, - this._tagService, - ); - - Future shareLink(List remoteIds, BuildContext context) async { - unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))); - } - - Future favorite(List remoteIds) async { - await _assetApiRepository.updateFavorite(remoteIds, true); - await _remoteAssetRepository.updateFavorite(remoteIds, true); - } - - Future unFavorite(List remoteIds) async { - await _assetApiRepository.updateFavorite(remoteIds, false); - await _remoteAssetRepository.updateFavorite(remoteIds, false); - } - - Future archive(List remoteIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .archive); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive); - } - - Future unArchive(List remoteIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .timeline); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline); - } - - Future moveToLockFolder(List remoteIds, List localIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .locked); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked); - - // Ask user if they want to delete local copies - if (localIds.isNotEmpty) { - await _deleteLocalAssets(localIds); - } - } - - Future removeFromLockFolder(List remoteIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .timeline); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline); - } - - Future trash(List remoteIds) async { - await _assetApiRepository.delete(remoteIds, false); - await _remoteAssetRepository.trash(remoteIds); - } - - Future restoreTrash(List ids) async { - await _assetApiRepository.restoreTrash(ids); - await _remoteAssetRepository.restoreTrash(ids); - } + const ActionService(this._assetApiRepository, this._remoteAssetRepository); Future emptyTrash(String userId) async { final count = await _assetApiRepository.emptyTrash(); @@ -125,113 +27,6 @@ class ActionService { return count; } - Future trashRemoteAndDeleteLocal(List remoteIds, List localIds) async { - await _assetApiRepository.delete(remoteIds, false); - await _remoteAssetRepository.trash(remoteIds); - - if (localIds.isNotEmpty) { - await _deleteLocalAssets(localIds); - } - } - - Future deleteRemoteAndLocal(List remoteIds, List localIds) async { - await _assetApiRepository.delete(remoteIds, true); - await _remoteAssetRepository.delete(remoteIds); - - if (localIds.isNotEmpty) { - await _deleteLocalAssets(localIds); - } - } - - Future deleteLocal(List localIds) async { - return await _deleteLocalAssets(localIds); - } - - Future editLocation(List remoteIds, BuildContext context) async { - maplibre.LatLng? initialLatLng; - if (remoteIds.length == 1) { - final exif = await _remoteAssetRepository.getExif(remoteIds[0]); - - if (exif?.latitude != null && exif?.longitude != null) { - initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!); - } - } - - final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); - - if (location == null) { - return false; - } - - await _assetApiRepository.updateLocation(remoteIds, location); - await _remoteAssetRepository.updateLocation(remoteIds, location); - - return true; - } - - Future editDateTime(List remoteIds, BuildContext context) async { - DateTime? initialDate; - String? timeZone; - Duration? offset; - - if (remoteIds.length == 1) { - final assetId = remoteIds.first; - final asset = await _remoteAssetRepository.get(assetId); - if (asset == null) { - return false; - } - - final exifData = await _remoteAssetRepository.getExif(assetId); - - // Use EXIF timezone information if available (matching web app and display behavior) - DateTime dt = asset.createdAt.toLocal(); - offset = dt.timeZoneOffset; - - if (exifData?.dateTimeOriginal != null) { - timeZone = exifData!.timeZone; - (dt, offset) = applyTimezoneOffset(dateTime: exifData.dateTimeOriginal!, timeZone: exifData.timeZone); - } - - initialDate = dt; - } - - final dateTime = await showDateTimePicker( - context: context, - initialDateTime: initialDate, - initialTZ: timeZone, - initialTZOffset: offset, - ); - - if (dateTime == null) { - return false; - } - - await applyDateTime(remoteIds, dateTime); - - return true; - } - - @visibleForTesting - Future applyDateTime(List remoteIds, String dateTime) async { - final parsedDateTime = DateTime.parse(dateTime); - final offset = RegExp(r'[+-]\d{2}:\d{2}$').firstMatch(dateTime)?.group(0); - - await _assetApiRepository.updateDateTime(remoteIds, dateTime); - await _remoteAssetRepository.updateDateTime( - remoteIds, - parsedDateTime, - timeZone: offset == null ? null : 'UTC$offset', - ); - } - - Future removeFromAlbum(List remoteIds, String albumId) async { - final result = await _albumApiRepository.removeAssets(albumId, remoteIds); - if (result.removed.isNotEmpty) { - await _remoteAlbumRepository.removeAssets(albumId, result.removed); - } - return result.removed.length; - } - Future updateDescription(String assetId, String description) async { // update remote first, then local to ensure consistency await _assetApiRepository.updateDescription(assetId, description); @@ -247,82 +42,4 @@ class ActionService { return true; } - - Future tagAssets(List remoteIds, BuildContext context) async { - final tagResults = await showTagPickerModal(context: context); - if (tagResults == null) { - // user cancelled - return null; - } - - final selectedTagIds = Set.from(tagResults.$1); - final selectedNewTagValues = tagResults.$2; - - if (selectedNewTagValues.isNotEmpty) { - final upsertedTags = await _tagService.upsertTags(selectedNewTagValues.toList()); - selectedTagIds.addAll(upsertedTags.map((t) => t.id)); - } - if (selectedTagIds.isEmpty) { - return 0; - } - return _tagService.bulkTagAssets(remoteIds, selectedTagIds.toList()); - } - - Future stack(String userId, List remoteIds) async { - final stack = await _assetApiRepository.stack(remoteIds); - await _remoteAssetRepository.stack(userId, stack); - } - - Future unStack(List stackIds) async { - await _remoteAssetRepository.unStack(stackIds); - await _assetApiRepository.unStack(stackIds); - } - - Future shareAssets( - List assets, - BuildContext context, { - ShareAssetType fileType = ShareAssetType.original, - Completer? cancelCompleter, - void Function(double progress)? onAssetDownloadProgress, - }) { - return _assetMediaRepository.shareAssets( - assets, - context, - fileType: fileType, - cancelCompleter: cancelCompleter, - onAssetDownloadProgress: onAssetDownloadProgress, - ); - } - - Future> downloadAll(List assets) { - return _downloadRepository.downloadAllAssets(assets); - } - - Future setAlbumCover(String albumId, String assetId) async { - final owner = await _remoteAlbumRepository.getOwner(albumId); - final updatedAlbum = await _albumApiRepository.updateAlbum(albumId, owner, thumbnailAssetId: assetId); - await _remoteAlbumRepository.update(updatedAlbum); - return true; - } - - Future applyEdits(String remoteId, List edits) async { - if (edits.isEmpty) { - await _assetApiRepository.removeEdits(remoteId); - } else { - await _assetApiRepository.editAsset(remoteId, edits); - } - } - - Future _deleteLocalAssets(List localIds) async { - final deletedIds = await _assetMediaRepository.deleteAll(localIds); - if (deletedIds.isEmpty) { - return 0; - } - if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { - await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds); - } else { - await _localAssetRepository.delete(deletedIds); - } - return deletedIds.length; - } } diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index ab05ffc18f..f5e4c5c33e 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -53,7 +53,7 @@ class ApiService { _apiClient.client = NetworkRepository.client; } - setEndpoint(String endpoint) { + void setEndpoint(String endpoint) { _apiClient.basePath = endpoint; _apiClient.client = NetworkRepository.client; usersApi = UsersApi(_apiClient); @@ -118,7 +118,7 @@ class ApiService { } try { - await setEndpoint(serverUrl); + setEndpoint(serverUrl); await serverInfoApi.pingServer().timeout(const Duration(seconds: 5)); } on TimeoutException catch (_) { return false; @@ -155,7 +155,7 @@ class ApiService { } Future setDeviceInfoHeader() async { - DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); if (Platform.isIOS) { final iosInfo = await deviceInfoPlugin.iosInfo; diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 0de22fd124..0e02c7b1b9 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -5,8 +5,8 @@ import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index ec731c6f68..5312107f6c 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -135,12 +135,12 @@ class BackgroundUploadService { if (!_taskStatusController.isClosed) { _taskStatusController.add(update); } - _handleTaskStatusUpdate(update); + unawaited(_handleTaskStatusUpdate(update)); } void dispose() { - _taskStatusController.close(); - _taskProgressController.close(); + unawaited(_taskStatusController.close()); + unawaited(_taskProgressController.close()); } /// Enqueue tasks to the background upload queue @@ -171,7 +171,7 @@ class BackgroundUploadService { const batchSize = 100; final batch = candidates.take(batchSize).toList(); - List tasks = []; + final List tasks = []; for (final asset in batch) { final task = await getUploadTask(asset); @@ -205,7 +205,7 @@ class BackgroundUploadService { return _uploadRepository.start(); } - void _handleTaskStatusUpdate(TaskStatusUpdate update) async { + Future _handleTaskStatusUpdate(TaskStatusUpdate update) async { switch (update.status) { case TaskStatus.complete: unawaited(_handleLivePhoto(update)); @@ -219,8 +219,6 @@ class BackgroundUploadService { } } - break; - default: break; } @@ -295,7 +293,7 @@ class BackgroundUploadService { final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name); final originalFileName = p.setExtension(fileName, extension); - String metadata = UploadTaskMetadata( + final String metadata = UploadTaskMetadata( localAssetId: asset.id, isLivePhotos: entity.isLivePhoto, livePhotoVideoId: '', diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index b84d6ebfe8..f38b20cc21 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_slow_async_io + import 'dart:async'; import 'dart:io'; @@ -63,7 +65,7 @@ class DownloadService { onVideoDownloadStatus?.call(update); } - void _onLivePhotoRecordComplete(TaskRecord record) async { + Future _onLivePhotoRecordComplete(TaskRecord record) async { final livePhotosId = LivePhotosMetadata.fromJson(record.task.metaData).id; await _saveLivePhotos(livePhotosId); } diff --git a/mobile/lib/services/folder.service.dart b/mobile/lib/services/folder.service.dart index 543c7231d6..011acbe869 100644 --- a/mobile/lib/services/folder.service.dart +++ b/mobile/lib/services/folder.service.dart @@ -18,7 +18,7 @@ class FolderService { final paths = await _folderApiRepository.getAllUniquePaths(); // Create folder structure - Map> folderMap = {}; + final Map> folderMap = {}; for (String fullPath in paths) { if (fullPath == '/') { @@ -30,12 +30,12 @@ class FolderService { fullPath = '/$fullPath'; } - List segments = fullPath.split('/')..removeWhere((s) => s.isEmpty); + final List segments = fullPath.split('/')..removeWhere((s) => s.isEmpty); String currentPath = ''; for (int i = 0; i < segments.length; i++) { - String parentPath = currentPath.isEmpty ? '_root_' : currentPath; + final String parentPath = currentPath.isEmpty ? '_root_' : currentPath; currentPath = i == 0 ? '/${segments[i]}' : '$currentPath/${segments[i]}'; if (!folderMap.containsKey(parentPath)) { @@ -55,7 +55,7 @@ class FolderService { } void attachSubfolders(RecursiveFolder folder) { - String fullPath = folder.path.isEmpty ? '/${folder.name}' : '${folder.path}/${folder.name}'; + final String fullPath = folder.path.isEmpty ? '/${folder.name}' : '${folder.path}/${folder.name}'; if (folderMap.containsKey(fullPath)) { folder.subfolders.addAll(folderMap[fullPath]!); @@ -67,7 +67,7 @@ class FolderService { } } - List rootSubfolders = folderMap['_root_'] ?? []; + final List rootSubfolders = folderMap['_root_'] ?? []; // Sort root subfolders based on order parameter rootSubfolders.sort((a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name)); @@ -83,7 +83,7 @@ class FolderService { if (folder is RecursiveFolder) { String fullPath = folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}'; fullPath = fullPath[0] == '/' ? fullPath.substring(1) : fullPath; - var result = await _folderApiRepository.getAssetsForPath(fullPath); + final result = await _folderApiRepository.getAssetsForPath(fullPath); if (order == SortOrder.desc) { result.sort((a, b) => b.createdAt.compareTo(a.createdAt)); diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index cce1241473..11cbd03017 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -35,6 +35,7 @@ class UploadCallbacks { } final foregroundUploadServiceProvider = Provider((ref) { + // ignore: dispose-provided-instances return ForegroundUploadService( ref.watch(uploadRepositoryProvider), ref.watch(storageRepositoryProvider), @@ -383,7 +384,6 @@ class ForegroundUploadService { if (result.isSuccess && result.remoteAssetId != null) { callbacks.onSuccess?.call(asset.localId!, result.remoteAssetId!); } else if (result.isCancelled) { - _logger.warning(() => "Backup was cancelled by the user"); shouldAbortUpload = true; } else if (result.errorMessage != null) { _logger.severe( @@ -398,7 +398,7 @@ class ForegroundUploadService { } } } catch (error, stackTrace) { - _logger.severe(() => "Error backup asset: ${error.toString()}", stackTrace); + _logger.severe(() => "Error backup asset: $error", stackTrace); callbacks.onError?.call(asset.localId!, error.toString()); } finally { if (Platform.isIOS) { @@ -406,7 +406,7 @@ class ForegroundUploadService { await file?.delete(); await livePhotoFile?.delete(); } catch (error, stackTrace) { - _logger.severe(() => "ERROR deleting file: ${error.toString()}", stackTrace); + _logger.severe(() => "ERROR deleting file: $error", stackTrace); } } } @@ -419,6 +419,7 @@ class ForegroundUploadService { void Function(int bytes, int totalBytes)? onProgress, }) async { try { + // ignore: avoid_slow_async_io final stats = await file.stat(); final fileCreatedAt = stats.changed; final fileModifiedAt = stats.modified; diff --git a/mobile/lib/services/gcast.service.dart b/mobile/lib/services/gcast.service.dart index dcf7685237..d9fc44a34d 100644 --- a/mobile/lib/services/gcast.service.dart +++ b/mobile/lib/services/gcast.service.dart @@ -62,7 +62,6 @@ class GCastService { switch (message['type']) { case "MEDIA_STATUS": _handleMediaStatus(message); - break; } } @@ -77,13 +76,10 @@ class GCastService { switch (status['playerState']) { case "PLAYING": onCastState?.call(CastState.playing); - break; case "PAUSED": onCastState?.call(CastState.paused); - break; case "BUFFERING": onCastState?.call(CastState.buffering); - break; case "IDLE": onCastState?.call(CastState.idle); @@ -91,8 +87,6 @@ class GCastService { if (status["idleReason"] == "FINISHED") { _mediaStatusPollingTimer?.cancel(); } - - break; } if (status["media"] != null && status["media"]["duration"] != null) { @@ -147,7 +141,7 @@ class GCastService { return bufferedExpiration.isAfter(DateTime.now()); } - void loadMedia(RemoteAsset asset, bool reload) async { + Future loadMedia(RemoteAsset asset, bool reload) async { if (!isConnected) { return; } else if (asset.id == currentAssetId && !reload) { diff --git a/mobile/lib/services/immich_logger.service.dart b/mobile/lib/services/immich_logger.service.dart index fab4b9966a..bfc9bd9b51 100644 --- a/mobile/lib/services/immich_logger.service.dart +++ b/mobile/lib/services/immich_logger.service.dart @@ -39,6 +39,10 @@ abstract final class ImmichLogger { await io.close(); } + if (!context.mounted) { + return; + } + final box = context.findRenderObject() as RenderBox?; // Share file diff --git a/mobile/lib/services/map.service.dart b/mobile/lib/services/map.service.dart index 5b50e8a890..b439af8668 100644 --- a/mobile/lib/services/map.service.dart +++ b/mobile/lib/services/map.service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -11,7 +13,7 @@ class MapService with ErrorLoggerMixin { final logger = Logger("MapService"); MapService(this._apiService) { - _setMapUserAgentHeader(); + unawaited(_setMapUserAgentHeader()); } Future _setMapUserAgentHeader() async { diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index 0330c8485c..e64c529f26 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -34,7 +34,7 @@ class SearchService { model: model, ); } catch (e) { - dPrint(() => "[ERROR] [getSearchSuggestions] ${e.toString()}"); + dPrint(() => "[ERROR] [getSearchSuggestions] $e"); return []; } } diff --git a/mobile/lib/services/server_info.service.dart b/mobile/lib/services/server_info.service.dart index 460e135421..fb8b347caa 100644 --- a/mobile/lib/services/server_info.service.dart +++ b/mobile/lib/services/server_info.service.dart @@ -21,7 +21,7 @@ class ServerInfoService { return ServerDiskInfo.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getDiskInfo] ${e.toString()}"); + dPrint(() => "Error [getDiskInfo] $e"); } return null; } @@ -33,7 +33,7 @@ class ServerInfoService { return ServerVersion.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getServerVersion] ${e.toString()}"); + dPrint(() => "Error [getServerVersion] $e"); } return null; } @@ -45,7 +45,7 @@ class ServerInfoService { return ServerFeatures.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getServerFeatures] ${e.toString()}"); + dPrint(() => "Error [getServerFeatures] $e"); } return null; } @@ -57,7 +57,7 @@ class ServerInfoService { return ServerConfig.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getServerConfig] ${e.toString()}"); + dPrint(() => "Error [getServerConfig] $e"); } return null; } diff --git a/mobile/lib/services/share_intent_service.dart b/mobile/lib/services/share_intent_service.dart index fca5c4a188..a5ab5d8bc9 100644 --- a/mobile/lib/services/share_intent_service.dart +++ b/mobile/lib/services/share_intent_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/repositories/share_handler.repository.dart'; @@ -12,6 +14,6 @@ class ShareIntentService { void init() { shareHandlerRepository.onSharedMedia = onSharedMedia; - shareHandlerRepository.init(); + unawaited(shareHandlerRepository.init()); } } diff --git a/mobile/lib/repositories/toast.repository.dart b/mobile/lib/services/toast.service.dart similarity index 91% rename from mobile/lib/repositories/toast.repository.dart rename to mobile/lib/services/toast.service.dart index 0cca50fdec..2b61a945ff 100644 --- a/mobile/lib/repositories/toast.repository.dart +++ b/mobile/lib/services/toast.service.dart @@ -9,8 +9,8 @@ class ToastOption { const ToastOption({this.timeout, this.onUndo}); } -class ToastRepository { - const ToastRepository(); +class ToastService { + const ToastService(); FutureOr success(String message, {ToastOption? toast}) { snackbar.success(message, duration: toast?.timeout); diff --git a/mobile/lib/services/view_intent.service.dart b/mobile/lib/services/view_intent.service.dart index 22a3407e5a..e822d1ebb6 100644 --- a/mobile/lib/services/view_intent.service.dart +++ b/mobile/lib/services/view_intent.service.dart @@ -61,6 +61,7 @@ class ViewIntentService { try { final file = File(path); + // ignore: avoid_slow_async_io if (await file.exists()) { await file.delete(); } diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart index 7f7c4d05d7..088679b508 100644 --- a/mobile/lib/theme/dynamic_theme.dart +++ b/mobile/lib/theme/dynamic_theme.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; - +import 'package:flutter/material.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/debug_print.dart'; diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 0e5a3123e7..28a9442dc7 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -8,30 +8,25 @@ 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/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/cast.action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; +import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; +import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; +import 'package:immich_mobile/presentation/actions/slideshow.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; class ActionButtonContext { @@ -85,9 +80,7 @@ enum ActionButtonType { removeFromLockFolder, removeFromAlbum, restoreTrash, - trash, deleteLocal, - deletePermanent, delete, advancedInfo; @@ -112,25 +105,12 @@ enum ActionButtonType { !context.isInLockedView && // context.asset.hasRemote && // !context.asset.hasLocal, - ActionButtonType.trash => - context.isOwner && // - !context.isInLockedView && // - context.asset.hasRemote && // - context.isTrashEnabled && // - context.timelineOrigin != TimelineOrigin.trash, ActionButtonType.restoreTrash => context.isOwner && // !context.isInLockedView && // context.asset.hasRemote && // context.timelineOrigin == TimelineOrigin.trash, - ActionButtonType.deletePermanent => - context.isOwner && // - context.asset.hasRemote && // - (!context.isTrashEnabled || context.timelineOrigin == TimelineOrigin.trash || context.isInLockedView), - ActionButtonType.delete => - context.isOwner && // - !context.isInLockedView && // - context.asset.hasRemote, + ActionButtonType.delete => true, ActionButtonType.moveToLockFolder => context.isOwner && // !context.isInLockedView && // @@ -141,7 +121,7 @@ enum ActionButtonType { context.asset.hasRemote, ActionButtonType.deleteLocal => !context.isInLockedView && // - context.asset.hasLocal, + context.asset.isMerged, ActionButtonType.upload => !context.isInLockedView && // context.asset.storage == AssetState.local, @@ -192,79 +172,36 @@ enum ActionButtonType { bool menuItem = false, ]) { return switch (this) { - ActionButtonType.advancedInfo => ActionMenuItemWidget(action: AssetDebugAction(assets: [context.asset])), - ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.shareLink => ShareLinkActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.advancedInfo => ActionMenuItem(action: AssetDebugAction(source: context.source)), + ActionButtonType.share => ActionMenuItem(action: ShareAction(source: context.source)), + ActionButtonType.shareLink => ActionMenuItem(action: ShareLinkAction(source: context.source)), + ActionButtonType.slideshow => const ActionMenuItem(action: SlideshowAction()), + ActionButtonType.archive || + ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)), + ActionButtonType.download => ActionMenuItem(action: DownloadAction(source: context.source)), + ActionButtonType.restoreTrash => ActionMenuItem(action: RestoreAction(source: context.source)), + ActionButtonType.delete => ActionMenuItem(action: DeleteAction(source: context.source)), + ActionButtonType.moveToLockFolder || + ActionButtonType.removeFromLockFolder => ActionMenuItem(action: LockAction(source: context.source)), + ActionButtonType.deleteLocal => ActionMenuItem(action: CleanupLocalAction(source: context.source)), + ActionButtonType.upload => ActionMenuItem( + action: UploadAction(source: context.source, showProgress: context.source == ActionSource.viewer), ), - ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.unarchive => UnArchiveActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.removeFromAlbum => ActionMenuItem( + action: RemoveFromAlbumAction(source: context.source, albumId: context.currentAlbum!.id), ), - ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.restoreTrash => RestoreActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.deletePermanent => DeletePermanentActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.deleteLocal => DeleteLocalActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton( - albumId: context.currentAlbum!.id, - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.setAlbumCover => SetAlbumCoverActionButton( - albumId: context.currentAlbum!.id, - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.setAlbumCover => ActionMenuItem( + action: SetAlbumCoverAction(source: context.source, albumId: context.currentAlbum!.id), ), ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.openInBrowser => OpenInBrowserActionButton( - remoteId: context.asset.remoteId!, - origin: context.timelineOrigin, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.unstack => ActionMenuItem(action: StackAction(source: context.source)), + ActionButtonType.openInBrowser => ActionMenuItem( + action: OpenInBrowserAction(remoteId: context.asset.remoteId!, origin: context.timelineOrigin), ), - ActionButtonType.similarPhotos => SimilarPhotosActionButton( - assetId: (context.asset as RemoteAsset).id, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.setProfilePicture => SetProfilePictureActionButton( - asset: context.asset, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.similarPhotos => ActionMenuItem( + action: SimilarPhotosAction(assetId: (context.asset as RemoteAsset).id), ), + ActionButtonType.setProfilePicture => ActionMenuItem(action: SetProfilePictureAction(asset: context.asset)), ActionButtonType.openInfo => BaseActionButton( label: 'info'.tr(), iconData: Icons.info_outline, @@ -283,7 +220,7 @@ enum ActionButtonType { EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt)); }, ), - ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.cast => const ActionMenuItem(action: CastAction()), }; } @@ -294,8 +231,6 @@ enum ActionButtonType { // 0: info ActionButtonType.openInfo => 0, // 10: move, remove, and delete - ActionButtonType.trash => 10, - ActionButtonType.deletePermanent => 10, ActionButtonType.removeFromLockFolder => 10, ActionButtonType.removeFromAlbum => 10, ActionButtonType.unstack => 10, @@ -323,7 +258,6 @@ class ActionButtonBuilder { ActionButtonType.archive, ActionButtonType.unarchive, ActionButtonType.restoreTrash, - ActionButtonType.deletePermanent, }; static List build(ActionButtonContext context) { diff --git a/mobile/lib/utils/async_mutex.dart b/mobile/lib/utils/async_mutex.dart index b97ab9b052..6c54c1220d 100644 --- a/mobile/lib/utils/async_mutex.dart +++ b/mobile/lib/utils/async_mutex.dart @@ -12,10 +12,12 @@ class AsyncMutex { Future run(Future Function() operation) { final completer = Completer(); _enqueued++; - _running.whenComplete(() { - _enqueued--; - completer.complete(Future.sync(operation)); - }); + unawaited( + _running.whenComplete(() { + _enqueued--; + completer.complete(Future.sync(operation)); + }), + ); return _running = completer.future; } } diff --git a/mobile/lib/utils/bytes_units.dart b/mobile/lib/utils/bytes_units.dart index 5eb15221fe..2c5c5f18a9 100644 --- a/mobile/lib/utils/bytes_units.dart +++ b/mobile/lib/utils/bytes_units.dart @@ -22,6 +22,6 @@ String formatHumanReadableBytes(int bytes, int decimals) { return "0 B"; } const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"]; - var i = (log(bytes) / log(1024)).floor(); + final i = (log(bytes) / log(1024)).floor(); return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}'; } diff --git a/mobile/lib/utils/diff.dart b/mobile/lib/utils/diff.dart index ea20de16cc..fae3a5d516 100644 --- a/mobile/lib/utils/diff.dart +++ b/mobile/lib/utils/diff.dart @@ -16,7 +16,8 @@ Future diffSortedLists( assert(la.isSorted(compare), "first argument must be sorted"); assert(lb.isSorted(compare), "second argument must be sorted"); bool diff = false; - int i = 0, j = 0; + int i = 0; + int j = 0; for (; i < la.length && j < lb.length;) { final int order = compare(la[i], lb[j]); if (order == 0) { @@ -53,7 +54,8 @@ bool diffSortedListsSync( assert(la.isSorted(compare), "first argument must be sorted"); assert(lb.isSorted(compare), "second argument must be sorted"); bool diff = false; - int i = 0, j = 0; + int i = 0; + int j = 0; for (; i < la.length && j < lb.length;) { final int order = compare(la[i], lb[j]); if (order == 0) { diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart index fa2dedf383..c3440cce1a 100644 --- a/mobile/lib/utils/editor.utils.dart +++ b/mobile/lib/utils/editor.utils.dart @@ -50,10 +50,10 @@ typedef NormalizedTransform = ({double rotation, bool mirrorHorizontal, bool mir NormalizedTransform normalizeTransformEdits(List edits) { final matrix = buildAffineFromEdits(edits); - double a = matrix.a; - double b = matrix.b; - double c = matrix.c; - double d = matrix.d; + final double a = matrix.a; + final double b = matrix.b; + final double c = matrix.c; + final double d = matrix.d; final rotation = ((isCloseToZero(a) ? asin(c) : acos(a)) * 180) / pi; diff --git a/mobile/lib/utils/error_handler.dart b/mobile/lib/utils/error_handler.dart index d82e480575..dcac1431ab 100644 --- a/mobile/lib/utils/error_handler.dart +++ b/mobile/lib/utils/error_handler.dart @@ -24,7 +24,7 @@ void handleError(Object error, {StackTrace? stack, String? description}) { ); final String message; - if (serverErrorMessage(error) case String serverMessage) { + if (serverErrorMessage(error) case final String serverMessage) { message = serverMessage; } else if (isConnectionError(error)) { message = StaticTranslations.instance.login_form_server_error; diff --git a/mobile/lib/utils/hooks/app_settings_update_hook.dart b/mobile/lib/utils/hooks/app_settings_update_hook.dart index c498b60b06..9d7cc4b064 100644 --- a/mobile/lib/utils/hooks/app_settings_update_hook.dart +++ b/mobile/lib/utils/hooks/app_settings_update_hook.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:immich_mobile/entities/store.entity.dart'; @@ -7,7 +9,7 @@ ValueNotifier useAppSettingsState(AppSettingsEnum key) { final notifier = useState(Store.get(key.storeKey, key.defaultValue)); // Listen to changes to the notifier and update app settings - useValueChanged(notifier.value, (_, __) => Store.put(key.storeKey, notifier.value)); + useValueChanged(notifier.value, (_, __) => unawaited(Store.put(key.storeKey, notifier.value))); return notifier; } diff --git a/mobile/lib/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart index 663bca3dbf..cea901bd2e 100644 --- a/mobile/lib/utils/hooks/crop_controller_hook.dart +++ b/mobile/lib/utils/hooks/crop_controller_hook.dart @@ -1,7 +1,8 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:crop_image/crop_image.dart'; import 'dart:ui'; // Import the dart:ui library for Rect +import 'package:crop_image/crop_image.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + /// A hook that provides a [CropController] instance. CropController useCropController() { return useMemoized(() => CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1))); diff --git a/mobile/lib/utils/image_converter.dart b/mobile/lib/utils/image_converter.dart index 6711e2bd56..1a3a00130a 100644 --- a/mobile/lib/utils/image_converter.dart +++ b/mobile/lib/utils/image_converter.dart @@ -12,16 +12,18 @@ import 'package:flutter/material.dart'; Future imageToUint8List(Image image) async { final Completer completer = Completer(); image.image - .resolve(const ImageConfiguration()) + .resolve(ImageConfiguration.empty) .addListener( ImageStreamListener((ImageInfo info, bool _) { - info.image.toByteData(format: ImageByteFormat.png).then((byteData) { - if (byteData != null) { - completer.complete(byteData.buffer.asUint8List()); - } else { - completer.completeError('Failed to convert image to bytes'); - } - }); + unawaited( + info.image.toByteData(format: ImageByteFormat.png).then((byteData) { + if (byteData != null) { + completer.complete(byteData.buffer.asUint8List()); + } else { + completer.completeError('Failed to convert image to bytes'); + } + }), + ); }, onError: (exception, stackTrace) => completer.completeError(exception)), ); return completer.future; diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 7a7ee03f2b..4f584061d7 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -12,7 +12,7 @@ String getThumbnailUrlForRemoteId( bool edited = true, String? thumbhash, }) { - final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.toString()}&edited=$edited'; + final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=$type&edited=$edited'; return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url; } diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index 6213b214a9..d5e6f957a5 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -68,7 +68,11 @@ class MapUtils { bool silent = false, }) async { try { - bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + if (!serviceEnabled && !silent) { unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog())); return (null, LocationPermission.deniedForever); @@ -78,6 +82,10 @@ class MapUtils { bool shouldRequestPermission = false; if (permission == LocationPermission.denied && !silent) { + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + shouldRequestPermission = await showDialog( context: context, builder: (context) => _LocationPermissionDisabledDialog(), @@ -95,7 +103,7 @@ class MapUtils { return (null, LocationPermission.deniedForever); } - Position currentUserLocation = await Geolocator.getCurrentPosition( + final Position currentUserLocation = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 711a4a3163..e92b2afd12 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -56,9 +56,9 @@ void upgradeDto(dynamic value, String targetType) { }); } -addDefault(dynamic value, String keys, dynamic defaultValue) { +void addDefault(dynamic value, String keys, dynamic defaultValue) { // Loop through the keys and assign the default value if the key is not present - List keyList = keys.split('.'); + final List keyList = keys.split('.'); dynamic current = value; for (int i = 0; i < keyList.length - 1; i++) { diff --git a/mobile/lib/utils/people.utils.dart b/mobile/lib/utils/people.utils.dart index ddd1867269..18ae8a4528 100644 --- a/mobile/lib/utils/people.utils.dart +++ b/mobile/lib/utils/people.utils.dart @@ -5,8 +5,8 @@ import 'package:immich_mobile/presentation/widgets/people/person_edit_birthday_m import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; String formatAge(DateTime birthDate, DateTime referenceDate) { - int ageInYears = _calculateAge(birthDate, referenceDate); - int ageInMonths = _calculateAgeInMonths(birthDate, referenceDate); + final int ageInYears = _calculateAge(birthDate, referenceDate); + final int ageInMonths = _calculateAgeInMonths(birthDate, referenceDate); if (ageInMonths <= 11) { return "person_age_months".t(args: {'months': ageInMonths.toString()}); diff --git a/mobile/lib/utils/timezone.dart b/mobile/lib/utils/timezone.dart index 3e8c42d1b2..980d51de8e 100644 --- a/mobile/lib/utils/timezone.dart +++ b/mobile/lib/utils/timezone.dart @@ -21,7 +21,7 @@ import 'package:timezone/timezone.dart'; return (dt, dt.timeZoneOffset); } on LocationNotFoundException { // Handle UTC offset format (e.g., "UTC+08:00") - RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); + final RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); final m = re.firstMatch(timeZone); if (m != null) { final hours = int.parse(m.group(1) ?? '0'); diff --git a/mobile/lib/widgets/activities/comment_bubble.dart b/mobile/lib/widgets/activities/comment_bubble.dart index 95cff7b87d..5431420741 100644 --- a/mobile/lib/widgets/activities/comment_bubble.dart +++ b/mobile/lib/widgets/activities/comment_bubble.dart @@ -28,13 +28,17 @@ class CommentBubble extends ConsumerWidget { final isLike = activity.type == ActivityType.like; final bgColor = isOwn ? context.colorScheme.primaryContainer : context.colorScheme.surfaceContainer; - final activityNotifier = ref.read( + final activityNotifier = ref.watch( albumActivityProvider((album.id, isAssetActivity ? activity.assetId : null)).notifier, ); Future openAssetViewer() async { final activityService = ref.read(activityServiceProvider); final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); + if (!context.mounted) { + return; + } + if (route != null) { await context.pushRoute(route); } diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart index 2025fa7583..be908259d4 100644 --- a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -30,12 +30,12 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget { height: 50, child: ListView.builder( scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { + itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(right: 4.0), child: UserCircleAvatar(user: sharedUsers[index], size: 36, hasBorder: true), ); - }), + }, itemCount: sharedUsers.length, ), ), diff --git a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart index a84dfbae37..12105aa498 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart @@ -11,7 +11,7 @@ class ThumbnailPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) { - var gradientColors = [ + final gradientColors = [ context.colorScheme.surfaceContainer, context.colorScheme.surfaceContainer.darken(amount: .1), ]; diff --git a/mobile/lib/widgets/asset_viewer/animated_play_pause.dart b/mobile/lib/widgets/asset_viewer/animated_play_pause.dart index 4be7f49b5a..bad8a6345c 100644 --- a/mobile/lib/widgets/asset_viewer/animated_play_pause.dart +++ b/mobile/lib/widgets/asset_viewer/animated_play_pause.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -27,9 +28,9 @@ class AnimatedPlayPauseState extends State with SingleTickerP super.didUpdateWidget(oldWidget); if (widget.playing != oldWidget.playing) { if (widget.playing) { - animationController.forward(); + unawaited(animationController.forward()); } else { - animationController.reverse(); + unawaited(animationController.reverse()); } } } diff --git a/mobile/lib/widgets/asset_viewer/cast_dialog.dart b/mobile/lib/widgets/asset_viewer/cast_dialog.dart index d406f29a22..f91d8f5b27 100644 --- a/mobile/lib/widgets/asset_viewer/cast_dialog.dart +++ b/mobile/lib/widgets/asset_viewer/cast_dialog.dart @@ -28,7 +28,7 @@ class CastDialog extends ConsumerWidget { width: 250, height: 250, child: FutureBuilder>( - future: ref.read(castProvider.notifier).getDevices(), + future: ref.watch(castProvider.notifier).getDevices(), builder: (context, snapshot) { if (snapshot.hasError) { return Text('error_saving_image'.tr(args: [snapshot.error.toString()])); diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index f48ee06fdd..18bb27d024 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -39,7 +39,7 @@ class ExifMap extends StatelessWidget { const zoomLevel = 16; if (Platform.isAndroid) { - Uri uri = Uri( + final Uri uri = Uri( scheme: 'geo', host: '$latitude,$longitude', queryParameters: {'z': '$zoomLevel', 'q': '$latitude,$longitude'}, @@ -48,8 +48,8 @@ class ExifMap extends StatelessWidget { return uri; } } else if (Platform.isIOS) { - var params = {'ll': '$latitude,$longitude', 'q': '$latitude,$longitude', 'z': '$zoomLevel'}; - Uri uri = Uri.https('maps.apple.com', '/', params); + final params = {'ll': '$latitude,$longitude', 'q': '$latitude,$longitude', 'z': '$zoomLevel'}; + final Uri uri = Uri.https('maps.apple.com', '/', params); if (await canLaunchUrl(uri)) { return uri; } @@ -73,7 +73,7 @@ class ExifMap extends StatelessWidget { assetMarkerRemoteId: markerId, assetThumbhash: markerAssetThumbhash, onTap: (tapPosition, latLong) async { - Uri? uri = await createCoordinatesUri(); + final Uri? uri = await createCoordinatesUri(); if (uri == null) { return; diff --git a/mobile/lib/widgets/asset_viewer/video_controls.dart b/mobile/lib/widgets/asset_viewer/video_controls.dart index 0f1e0e020d..14c4daf5d8 100644 --- a/mobile/lib/widgets/asset_viewer/video_controls.dart +++ b/mobile/lib/widgets/asset_viewer/video_controls.dart @@ -4,11 +4,11 @@ import 'package:async/async.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/duration_extensions.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; class VideoControls extends ConsumerStatefulWidget { @@ -99,7 +99,7 @@ class _VideoControlsState extends ConsumerState { }); ref.listen(_provider.select((v) => v.status), (_, __) => _hideTimer.reset()); - final notifier = ref.read(_provider.notifier); + final notifier = ref.watch(_provider.notifier); final isLoaded = duration != Duration.zero; return Padding( diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart index 84128ddde2..41a215fb99 100644 --- a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -20,7 +22,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { final bool isSelected = album.backupSelection == BackupSelection.selected; final bool isExcluded = album.backupSelection == BackupSelection.excluded; - buildTileColor() { + Color? buildTileColor() { if (isSelected) { return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); } else if (isExcluded) { @@ -30,7 +32,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { } } - buildIcon() { + Icon buildIcon() { if (isSelected) { return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary); } @@ -51,10 +53,10 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { return GestureDetector( onDoubleTap: () { - ref.watch(hapticFeedbackProvider.notifier).selectionClick(); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isExcluded) { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).deselectAlbum(album)); } else { if (album.id == 'isAll' || album.name == 'Recents') { ImmichToast.show( @@ -66,7 +68,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { return; } - ref.read(backupAlbumProvider.notifier).excludeAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).excludeAlbum(album)); } }, child: ListTile( @@ -75,9 +77,9 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { onTap: () { ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isSelected) { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).deselectAlbum(album)); } else { - ref.read(backupAlbumProvider.notifier).selectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).selectAlbum(album)); } }, leading: buildIcon(), @@ -85,7 +87,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { subtitle: buildSubtitle(), trailing: IconButton( onPressed: () { - context.pushRoute(LocalTimelineRoute(album: album)); + unawaited(context.pushRoute(LocalTimelineRoute(album: album))); }, icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), splashRadius: 25, 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 e77bc1869e..b390d2a423 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 @@ -29,21 +29,21 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - ServerDiskInfo backupState = ref.watch(backupProvider); + final ServerDiskInfo backupState = ref.watch(backupProvider); final theme = context.themeData; - bool isHorizontal = !context.isMobile; + final bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; final user = ref.watch(currentUserProvider); final isLoggingOut = useState(false); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); useEffect(() { - ref.read(backupProvider.notifier).updateDiskInfo(); - ref.read(currentUserProvider.notifier).refresh(); + unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); + unawaited(ref.read(currentUserProvider.notifier).refresh()); return null; }, []); - buildTopRow() { + SizedBox buildTopRow() { return SizedBox( height: 56, child: Stack( @@ -68,7 +68,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing}) { + ListTile buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing}) { return ListTile( dense: true, visualDensity: VisualDensity.standard, @@ -84,11 +84,11 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildSettingButton() { + ListTile buildSettingButton() { return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute())); } - buildFreeUpSpaceButton() { + ListTile buildFreeUpSpaceButton() { return buildActionButton( Icons.cleaning_services_outlined, "free_up_space", @@ -96,7 +96,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildAppLogButton() { + ListTile buildAppLogButton() { return buildActionButton( Icons.assignment_outlined, "profile_drawer_app_logs", @@ -104,7 +104,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildSignOutButton() { + ListTile buildSignOutButton() { return buildActionButton( Icons.logout_rounded, "sign_out", @@ -124,8 +124,15 @@ class ImmichAppBarDialog extends HookConsumerWidget { onOk: () async { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); + if (!context.mounted) { + return; + } ref.read(websocketProvider.notifier).disconnect(); + if (!context.mounted) { + return; + } + unawaited(context.replaceRoute(const LoginRoute())); }, ); @@ -171,7 +178,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildFooter() { + Padding buildFooter() { return Padding( padding: const EdgeInsets.only(top: 10, bottom: 20), child: Row( @@ -180,7 +187,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { InkWell( onTap: () { ContextHelper(context).pop(); - launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication); + unawaited(launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication)); }, child: Text("documentation", style: context.textTheme.bodySmall).tr(), ), @@ -188,7 +195,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { InkWell( onTap: () { ContextHelper(context).pop(); - launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication); + unawaited( + launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication), + ); }, child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(), ), @@ -197,6 +206,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { onTap: () async { ContextHelper(context).pop(); final packageInfo = await PackageInfo.fromPlatform(); + if (!context.mounted) { + return; + } + showLicensePage( context: context, applicationIcon: const Padding( @@ -213,7 +226,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildReadonlyMessage() { + Padding buildReadonlyMessage() { return Padding( padding: const EdgeInsets.only(left: 10.0, right: 10.0), child: ListTile( 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 d6881f519a..51cdd7c5a3 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 @@ -24,7 +24,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final user = ref.watch(currentUserProvider); - buildUserProfileImage() { + Widget buildUserProfileImage() { if (user == null) { return const CircleAvatar( radius: 20, @@ -42,17 +42,17 @@ class AppBarProfileInfoBox extends HookConsumerWidget { return userImage; } - pickUserProfileImage() async { + Future pickUserProfileImage() async { final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024); - if (image != null) { - var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image); + if (image != null && context.mounted) { + final success = await ref.read(uploadProfileImageProvider.notifier).upload(image); - if (success) { + if (success && context.mounted) { final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath; - ref.watch(authProvider.notifier).updateUserProfileImagePath(profileImagePath); + ref.read(authProvider.notifier).updateUserProfileImagePath(profileImagePath); if (user != null) { - ref.read(currentUserProvider.notifier).refresh(); + unawaited(ref.read(currentUserProvider.notifier).refresh()); } unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); @@ -61,7 +61,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { } void toggleReadonlyMode() { - final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); + final isReadonlyModeEnabled = ref.read(readonlyModeProvider); ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); context.scaffoldMessenger.showSnackBar( 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 a209d280c3..348e3aa14c 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,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -18,20 +20,20 @@ class AppBarServerInfo extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - ServerInfo serverInfoState = ref.watch(serverInfoProvider); + final ServerInfo serverInfoState = ref.watch(serverInfoProvider); final user = ref.watch(currentUserProvider); final bool showVersionWarning = ref.watch(versionWarningPresentProvider(user)); final appInfo = useState({}); - getPackageInfo() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); + Future getPackageInfo() async { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); appInfo.value = {"version": packageInfo.version, "buildNumber": packageInfo.buildNumber}; } useEffect(() { - getPackageInfo(); + unawaited(getPackageInfo()); return null; }, []); @@ -87,7 +89,7 @@ class _ServerInfoItem extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (icon != null) ...[icon as Widget, const SizedBox(width: 8)], + if (icon != null) ...[icon! as Widget, const SizedBox(width: 8)], Text( label, style: TextStyle( 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 179eab8e7d..806ac87c19 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,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -16,9 +17,11 @@ class ServerUpdateNotification extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final serverInfoState = ref.watch(serverInfoProvider); - Color errorColor = const Color.fromARGB(85, 253, 97, 83); - Color infoColor = context.isDarkTheme ? context.primaryColor.withAlpha(55) : context.primaryColor.withAlpha(25); - void openUpdateLink() { + const Color errorColor = Color.fromARGB(85, 253, 97, 83); + final Color infoColor = context.isDarkTheme + ? context.primaryColor.withAlpha(55) + : context.primaryColor.withAlpha(25); + Future openUpdateLink() { String url; if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) { url = kImmichLatestRelease; @@ -33,7 +36,7 @@ class ServerUpdateNotification extends HookConsumerWidget { } } - launchUrlString(url, mode: LaunchMode.externalApplication); + return launchUrlString(url, mode: LaunchMode.externalApplication); } return SizedBox( @@ -66,10 +69,10 @@ class ServerUpdateNotification extends HookConsumerWidget { serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[ const SizedBox(width: 8), TextButton( - onPressed: openUpdateLink, + onPressed: () => unawaited(openUpdateLink()), style: TextButton.styleFrom( padding: const EdgeInsets.all(4), - minimumSize: const Size(0, 0), + minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: serverInfoState.versionStatus == VersionStatus.clientOutOfDate diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 0ebd7bba93..3aedd55de5 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -41,7 +41,7 @@ class _DateTimePicker extends HookWidget { } } - Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset; + final Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset; if (tzOffset != null) { final offsetInMilli = tzOffset.inMilliseconds; @@ -80,7 +80,7 @@ class _DateTimePicker extends HookWidget { ) .toList(); - void pickDate() async { + Future pickDate() async { final now = DateTime.now(); // Handles cases where the date from the asset is far off in the future final initialDate = date.value.isAfter(now) ? now : date.value; @@ -90,7 +90,7 @@ class _DateTimePicker extends HookWidget { firstDate: DateTime(1800), lastDate: now, ); - if (newDate == null) { + if (newDate == null || !context.mounted) { return; } diff --git a/mobile/lib/widgets/common/dropdown_search_menu.dart b/mobile/lib/widgets/common/dropdown_search_menu.dart index bf0c75c8aa..54568c392a 100644 --- a/mobile/lib/widgets/common/dropdown_search_menu.dart +++ b/mobile/lib/widgets/common/dropdown_search_menu.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -114,7 +116,7 @@ class DropdownSearchMenu extends HookWidget { final bool highlight = AutocompleteHighlightedOption.of(context) == index; if (highlight) { SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - Scrollable.ensureVisible(context, alignment: 0.5); + unawaited(Scrollable.ensureVisible(context, alignment: 0.5)); }, debugLabel: 'AutocompleteOptions.ensureVisible'); } return Container( diff --git a/mobile/lib/widgets/common/immich_loading_indicator.dart b/mobile/lib/widgets/common/immich_loading_indicator.dart index 52f957f7e7..1fdc5213df 100644 --- a/mobile/lib/widgets/common/immich_loading_indicator.dart +++ b/mobile/lib/widgets/common/immich_loading_indicator.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; @@ -9,11 +11,12 @@ class ImmichLoadingIndicator extends HookWidget { @override Widget build(BuildContext context) { - final logoAnimationController = useAnimationController(duration: const Duration(seconds: 6)) - ..reverse() - ..repeat(); + final logoAnimationController = useAnimationController(duration: const Duration(seconds: 6)); + unawaited(logoAnimationController.reverse()); + unawaited(logoAnimationController.repeat()); - final borderAnimationController = useAnimationController(duration: const Duration(seconds: 6))..repeat(); + final borderAnimationController = useAnimationController(duration: const Duration(seconds: 6)); + unawaited(borderAnimationController.repeat()); return Container( height: 80, diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 6905b5b430..54495365db 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; @@ -10,8 +11,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.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'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -112,7 +113,7 @@ class _ProfileIndicator extends ConsumerWidget { final isIpad = defaultTargetPlatform == TargetPlatform.iOS && !context.isMobile; void toggleReadonlyMode() { - final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); + final isReadonlyModeEnabled = ref.read(readonlyModeProvider); ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); context.scaffoldMessenger.showSnackBar( @@ -192,11 +193,11 @@ class _BackupIndicator extends ConsumerWidget { } Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) { - final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled)); - final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none)); + final backupEnabled = ref.read(appConfigProvider.select((c) => c.backup.enabled)); + final hasError = ref.read(driftBackupProvider.select((state) => state.error != BackupError.none)); final isDarkTheme = context.isDarkTheme; final iconColor = isDarkTheme ? Colors.white : Colors.black; - final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); + final isUploading = ref.read(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); if (!backupEnabled) { return _BadgeLabel( @@ -304,13 +305,13 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with // Control animations based on sync status if (isSyncing) { if (!_rotationController.isAnimating) { - _rotationController.repeat(); + unawaited(_rotationController.repeat()); } _dismissalController.reset(); } else { _rotationController.stop(); if (_dismissalController.status == AnimationStatus.dismissed) { - _dismissalController.forward(); + unawaited(_dismissalController.forward()); } } diff --git a/mobile/lib/widgets/common/immich_toast.dart b/mobile/lib/widgets/common/immich_toast.dart index 1da07f419d..0a06de4d91 100644 --- a/mobile/lib/widgets/common/immich_toast.dart +++ b/mobile/lib/widgets/common/immich_toast.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; enum ToastType { info, success, error } class ImmichToast { - static show({ + static void show({ required BuildContext context, required String msg, ToastType toastType = ToastType.info, diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index 90dfd9c82a..0ceb98aa76 100644 --- a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -134,7 +134,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S Future.delayed(const Duration(milliseconds: 100), () { if (mounted) { - _slideController.forward(); + unawaited(_slideController.forward()); } }); } @@ -228,7 +228,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { @override void dispose() { - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); super.dispose(); } @@ -311,13 +311,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void _startAnimationCycle() { if (_isZoomingIn) { - _zoomController.forward().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.forward().then((_) { + unawaited(_loadNextAsset()); + }), + ); } else { - _zoomController.reverse().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.reverse().then((_) { + unawaited(_loadNextAsset()); + }), + ); } } diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index a9f9413c47..0e6829243c 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -13,11 +13,11 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/people.utils.dart'; class PersonSliverAppBar extends ConsumerStatefulWidget { const PersonSliverAppBar({ @@ -56,8 +56,8 @@ class _MesmerizingSliverAppBarState extends ConsumerState { @override Widget build(BuildContext context) { final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); - Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress); - List actionIconShadows = [ + final Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress); + final List actionIconShadows = [ if (_scrollProgress < 0.95) Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5)) else @@ -169,7 +169,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S Future.delayed(const Duration(milliseconds: 100), () { if (mounted) { - _slideController.forward(); + unawaited(_slideController.forward()); } }); } @@ -335,7 +335,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { @override void dispose() { - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); super.dispose(); } @@ -416,13 +416,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void _startAnimationCycle() { if (_isZoomingIn) { - _zoomController.forward().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.forward().then((_) { + unawaited(_loadNextAsset()); + }), + ); } else { - _zoomController.reverse().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.reverse().then((_) { + unawaited(_loadNextAsset()); + }), + ); } } 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 fee77dcd99..09a0367039 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -64,9 +64,9 @@ class _MesmerizingSliverAppBarState extends ConsumerState actionIconShadows = [ + final List actionIconShadows = [ if (_scrollProgress < 0.95) Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5)) else @@ -172,7 +172,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S Future.delayed(const Duration(milliseconds: 100), () { if (mounted) { - _slideController.forward(); + unawaited(_slideController.forward()); } }); } @@ -309,7 +309,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { @override void dispose() { - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); super.dispose(); } @@ -390,13 +390,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void _startAnimationCycle() { if (_isZoomingIn) { - _zoomController.forward().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.forward().then((_) { + unawaited(_loadNextAsset()); + }), + ); } else { - _zoomController.reverse().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.reverse().then((_) { + unawaited(_loadNextAsset()); + }), + ); } } diff --git a/mobile/lib/widgets/common/selection_sliver_app_bar.dart b/mobile/lib/widgets/common/selection_sliver_app_bar.dart index ac74e69e64..d60124041c 100644 --- a/mobile/lib/widgets/common/selection_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/selection_sliver_app_bar.dart @@ -23,7 +23,7 @@ class _SelectionSliverAppBarState extends ConsumerState { return !toExclude.contains(asset); }).toSet(); - onDone(Set selected) { + void onDone(Set selected) { ref.read(multiSelectProvider.notifier).reset(); context.pop>(selected); } diff --git a/mobile/lib/widgets/common/tag_picker.dart b/mobile/lib/widgets/common/tag_picker.dart index 0265cf7e6c..82228f5b0b 100644 --- a/mobile/lib/widgets/common/tag_picker.dart +++ b/mobile/lib/widgets/common/tag_picker.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; @@ -17,13 +17,13 @@ Future<(Set, Set)?> showTagPickerModal({required BuildContext co ); } -class _TagPickerModal extends HookConsumerWidget { +class _TagPickerModal extends HookWidget { final Set? initialSelection; const _TagPickerModal({this.initialSelection}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final selectedTagIds = useState>(initialSelection ?? {}); final newTagValues = useState>({}); @@ -86,7 +86,7 @@ class TagPicker extends HookConsumerWidget { final searchQuery = useState(''); final tags = ref.watch(tagProvider); final selectedTagIds = useState>(filter); - final borderRadius = const BorderRadius.all(Radius.circular(10)); + const borderRadius = BorderRadius.all(Radius.circular(10)); final selectedNewTagValues = useState>({}); return Column( @@ -125,7 +125,7 @@ class TagPicker extends HookConsumerWidget { // Create new tag tile return Padding( padding: const EdgeInsets.only(bottom: 2.0), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: isCreateSelected ? context.primaryColor : context.primaryColor.withAlpha(25), borderRadius: const BorderRadius.all(Radius.circular(10)), @@ -160,7 +160,7 @@ class TagPicker extends HookConsumerWidget { return Padding( padding: const EdgeInsets.only(bottom: 2.0), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: isSelected ? context.primaryColor : context.primaryColor.withAlpha(25), borderRadius: borderRadius, diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index c6e4f4719e..11257da77b 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -1,21 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -// ignore: must_be_immutable -class UserCircleAvatar extends ConsumerWidget { +class UserCircleAvatar extends StatelessWidget { final UserDto user; - double size; - bool hasBorder; - double opacity; + final double size; + final bool hasBorder; + final double opacity; - UserCircleAvatar({super.key, this.size = 44, this.hasBorder = false, this.opacity = 1, required this.user}); + const UserCircleAvatar({super.key, this.size = 44, this.hasBorder = false, this.opacity = 1, required this.user}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final userAvatarColor = user.avatarColor.toColor().withValues(alpha: opacity); final profileImageUrl = '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}'; diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 7ed9fa5f1c..229eaa7ec6 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -55,30 +55,37 @@ class ChangePasswordForm extends HookConsumerWidget { passwordController: passwordController, onPressed: () async { if (formKey.currentState!.validate()) { - var isSuccess = await ref + final isSuccess = await ref .read(authProvider.notifier) .changePassword(passwordController.value.text); - if (isSuccess) { - await ref.read(authProvider.notifier).logout(); - ref.read(websocketProvider.notifier).disconnect(); - - AutoRouter.of(context).back(); - - ImmichToast.show( - context: context, - msg: "login_password_changed_success".tr(), - toastType: ToastType.success, - gravity: ToastGravity.TOP, - ); - } else { + if (!isSuccess && context.mounted) { ImmichToast.show( context: context, msg: "login_password_changed_error".tr(), toastType: ToastType.error, gravity: ToastGravity.TOP, ); + return; } + + if (!context.mounted) { + return; + } + + await ref.read(authProvider.notifier).logout(); + if (!context.mounted) { + return; + } + + ref.read(websocketProvider.notifier).disconnect(); + AutoRouter.of(context).back(); + ImmichToast.show( + context: context, + msg: "login_password_changed_success".tr(), + toastType: ToastType.success, + gravity: ToastGravity.TOP, + ); } }, ), @@ -146,13 +153,13 @@ class ConfirmPasswordInput extends StatelessWidget { } } -class ChangePasswordButton extends ConsumerWidget { +class ChangePasswordButton extends StatelessWidget { final TextEditingController passwordController; final VoidCallback onPressed; const ChangePasswordButton({super.key, required this.passwordController, required this.onPressed}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return ElevatedButton( style: ElevatedButton.styleFrom( visualDensity: VisualDensity.standard, diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 79617f8fe4..e4c7516c43 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -70,13 +70,14 @@ class LoginForm extends HookConsumerWidget { final isOauthEnable = useState(false); final isPasswordLoginEnable = useState(false); final oAuthButtonLabel = useState('OAuth'); - final logoAnimationController = useAnimationController(duration: const Duration(seconds: 60))..repeat(); + final logoAnimationController = useAnimationController(duration: const Duration(seconds: 60)); + unawaited(logoAnimationController.repeat()); final serverInfo = ref.watch(serverInfoProvider); final warningMessage = useState(null); final loginFormKey = GlobalKey(); final ValueNotifier serverEndpoint = useState(null); - checkVersionMismatch() async { + Future checkVersionMismatch() async { try { final packageInfo = await PackageInfo.fromPlatform(); final appSemVer = SemVer.fromString(packageInfo.version); @@ -100,9 +101,15 @@ class LoginForm extends HookConsumerWidget { try { final endpoint = await ref.read(authProvider.notifier).validateServerUrl(serverUrl); + if (!context.mounted) { + return; + } // Fetch and load server config and features await ref.read(serverInfoProvider.notifier).getServerInfo(); + if (!context.mounted) { + return; + } final serverInfo = ref.read(serverInfoProvider); final features = serverInfo.serverFeatures; @@ -114,6 +121,10 @@ class LoginForm extends HookConsumerWidget { serverEndpoint.value = endpoint; } on ApiException catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: e.message ?? 'login_form_api_exception'.tr(), @@ -123,6 +134,10 @@ class LoginForm extends HookConsumerWidget { isOauthEnable.value = false; isPasswordLoginEnable.value = true; } on HandshakeException { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: 'login_form_handshake_exception'.tr(), @@ -132,6 +147,10 @@ class LoginForm extends HookConsumerWidget { isOauthEnable.value = false; isPasswordLoginEnable.value = true; } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: 'login_form_server_error'.tr(), @@ -151,13 +170,13 @@ class LoginForm extends HookConsumerWidget { return null; }, []); - populateTestLoginInfo() { + void populateTestLoginInfo() { emailController.text = 'demo@immich.app'; passwordController.text = 'demo'; serverEndpointController.text = 'https://demo.immich.app'; } - populateTestLoginInfo1() { + void populateTestLoginInfo1() { emailController.text = 'testuser@email.com'; passwordController.text = 'password'; serverEndpointController.text = 'http://10.1.15.216:2283/api'; @@ -177,8 +196,12 @@ class LoginForm extends HookConsumerWidget { } } - getManageMediaPermission() async { + Future getManageMediaPermission() async { final hasPermission = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); + if (!context.mounted) { + return; + } + if (!hasPermission) { await showDialog( context: context, @@ -226,7 +249,7 @@ class LoginForm extends HookConsumerWidget { bool isSyncRemoteDeletionsMode() => Platform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false); - login() async { + Future login() async { TextInput.finishAutofillContext(); // Invalidate all api repository provider instance to take into account new access token @@ -235,6 +258,10 @@ class LoginForm extends HookConsumerWidget { try { final result = await ref.read(authProvider.notifier).login(emailController.text, passwordController.text); + if (!context.mounted) { + return; + } + if (result.shouldChangePassword && !result.isAdmin) { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { @@ -243,12 +270,24 @@ class LoginForm extends HookConsumerWidget { await getManageMediaPermission(); } unawaited(handleSyncFlow()); + if (!context.mounted) { + return; + } + ref.read(websocketProvider.notifier).connect(); unawaited(ref.read(featureMessageServiceProvider).markSeen()); + if (!context.mounted) { + return; + } + unawaited(context.router.replaceAll([const TabShellRoute()])); return; } } catch (error) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: "login_form_failed_login".tr(), @@ -277,13 +316,13 @@ class LoginForm extends HookConsumerWidget { } Future generatePKCECodeChallenge(String codeVerifier) async { - var bytes = utf8.encode(codeVerifier); - var digest = sha256.convert(bytes); + final bytes = utf8.encode(codeVerifier); + final digest = sha256.convert(bytes); return base64Url.encode(digest.bytes).replaceAll('=', ''); } - oAuthLogin() async { - var oAuthService = ref.watch(oAuthServiceProvider); + Future oAuthLogin() async { + final oAuthService = ref.read(oAuthServiceProvider); String? oAuthServerUrl; final state = generateRandomString(32); @@ -303,6 +342,10 @@ class LoginForm extends HookConsumerWidget { } catch (error, stack) { log.severe('Error getting OAuth server Url: $error', stack); + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: "login_form_failed_get_oauth_server_config".tr(), @@ -316,22 +359,26 @@ class LoginForm extends HookConsumerWidget { try { final loginResponseDto = await oAuthService.oAuthLogin(oAuthServerUrl, state, codeVerifier); - if (loginResponseDto == null) { + if (loginResponseDto == null || !context.mounted) { return; } log.info("Finished OAuth login with response: ${loginResponseDto.userEmail}"); final isSuccess = await ref - .watch(authProvider.notifier) + .read(authProvider.notifier) .saveAuthInfo(accessToken: loginResponseDto.accessToken); - if (isSuccess) { + if (isSuccess && context.mounted) { await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); if (isSyncRemoteDeletionsMode()) { await getManageMediaPermission(); } unawaited(handleSyncFlow()); + if (!context.mounted) { + return; + } + unawaited(ref.read(featureMessageServiceProvider).markSeen()); unawaited(context.router.replaceAll([const TabShellRoute()])); return; @@ -339,6 +386,10 @@ class LoginForm extends HookConsumerWidget { } catch (error, stack) { log.severe('Error logging in with OAuth: $error', stack); + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: error.toString(), @@ -347,6 +398,10 @@ class LoginForm extends HookConsumerWidget { ); } finally {} } else { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: "login_form_failed_get_oauth_server_disable".tr(), @@ -357,8 +412,8 @@ class LoginForm extends HookConsumerWidget { } } - buildVersionCompatWarning() { - checkVersionMismatch(); + SingleChildRenderObjectWidget buildVersionCompatWarning() { + unawaited(checkVersionMismatch()); if (warningMessage.value == null) { return const SizedBox.shrink(); diff --git a/mobile/lib/widgets/forms/pin_input.dart b/mobile/lib/widgets/forms/pin_input.dart index c4f0d8f3b7..8bcfd51282 100644 --- a/mobile/lib/widgets/forms/pin_input.dart +++ b/mobile/lib/widgets/forms/pin_input.dart @@ -26,9 +26,9 @@ class PinInput extends StatelessWidget { @override Widget build(BuildContext context) { - getPinSize() { - final minimumPadding = 18.0; - final gapWidth = 3.0; + Size getPinSize() { + const minimumPadding = 18.0; + const gapWidth = 3.0; final screenWidth = context.width; final pinWidth = (screenWidth - (minimumPadding * 2) - (gapWidth * 5)) / (length ?? 6); diff --git a/mobile/lib/widgets/forms/pin_registration_form.dart b/mobile/lib/widgets/forms/pin_registration_form.dart index d126169aad..ebd299bcce 100644 --- a/mobile/lib/widgets/forms/pin_registration_form.dart +++ b/mobile/lib/widgets/forms/pin_registration_form.dart @@ -29,7 +29,7 @@ class PinRegistrationForm extends HookConsumerWidget { return true; } - createNewPinCode() async { + Future createNewPinCode() async { final isValid = validatePinCode(); if (!isValid) { hasError.value = true; @@ -42,6 +42,10 @@ class PinRegistrationForm extends HookConsumerWidget { onDone(); } catch (error) { hasError.value = true; + if (!context.mounted) { + return; + } + context.showSnackBar(SnackBar(content: Text(error.toString()))); } } diff --git a/mobile/lib/widgets/forms/pin_verification_form.dart b/mobile/lib/widgets/forms/pin_verification_form.dart index 2b7e3e8251..de2acaa1f7 100644 --- a/mobile/lib/widgets/forms/pin_verification_form.dart +++ b/mobile/lib/widgets/forms/pin_verification_form.dart @@ -29,7 +29,7 @@ class PinVerificationForm extends HookConsumerWidget { final hasError = useState(false); final isVerified = useState(false); - verifyPin(String pinCode) async { + Future verifyPin(String pinCode) async { final isUnlocked = await ref.read(authProvider.notifier).unlockPinCode(pinCode); if (isUnlocked) { diff --git a/mobile/lib/widgets/map/asset_marker_icon.dart b/mobile/lib/widgets/map/asset_marker_icon.dart index ff6058161b..75881195a4 100644 --- a/mobile/lib/widgets/map/asset_marker_icon.dart +++ b/mobile/lib/widgets/map/asset_marker_icon.dart @@ -69,15 +69,15 @@ class _PinPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - Paint primaryBrush = Paint() + final Paint primaryBrush = Paint() ..color = primaryColor ..style = PaintingStyle.fill; - Paint secondaryBrush = Paint() + final Paint secondaryBrush = Paint() ..color = secondaryColor ..style = PaintingStyle.fill; - Paint lineBrush = Paint() + final Paint lineBrush = Paint() ..color = primaryColor ..style = PaintingStyle.stroke ..strokeWidth = 2; 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 7866c0ecdc..e66f08c221 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -69,7 +69,7 @@ class _BorderedMapThumbnail extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - Container( + DecoratedBox( decoration: BoxDecoration( border: Border.fromBorderSide( BorderSide(width: 4, color: shouldHighlight ? context.colorScheme.onSurface : Colors.transparent), diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index 57f970b0d1..dda96efeaf 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -66,7 +66,7 @@ class _MapThemeOverrideState extends ConsumerState with Widget @override Widget build(BuildContext context) { _theme = widget.themeMode ?? ref.watch(mapStateNotifierProvider.select((v) => v.themeMode)); - var appTheme = ref.watch(immichThemeProvider); + final appTheme = ref.watch(immichThemeProvider); final locale = ref.watch(localeProvider); useValueChanged(_theme, (_, __) { diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index 7defb52264..84c2ac0100 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -2,12 +2,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; +import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; /// A non-interactive thumbnail of a map in the given coordinates with optional markers @@ -15,7 +14,7 @@ import 'package:maplibre_gl/maplibre_gl.dart'; /// User can provide either a [assetMarkerRemoteId] to display the asset's thumbnail or set /// [showMarkerPin] to true which would display a marker pin instead. If both are provided, /// [assetMarkerRemoteId] will take precedence -class MapThumbnail extends HookConsumerWidget { +class MapThumbnail extends HookWidget { final Function(Point, LatLng)? onTap; final LatLng centre; final String? assetMarkerRemoteId; @@ -44,7 +43,7 @@ class MapThumbnail extends HookConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final controller = useRef(null); final styleLoaded = useState(false); diff --git a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart index b9475a9ee2..08c300e9ae 100644 --- a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart +++ b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart @@ -209,7 +209,7 @@ class PhotoViewController implements PhotoViewControllerBase return; } _scaleAnimation = Tween(begin: from, end: to).animate(_scaleAnimationController); - _scaleAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _scaleAnimationController.value = 0.0; + unawaited(_scaleAnimationController.fling(velocity: 0.4)); } void animatePosition(Offset from, Offset to) { @@ -250,9 +251,8 @@ class PhotoViewCoreState extends State return; } _positionAnimation = Tween(begin: from, end: to).animate(_positionAnimationController); - _positionAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _positionAnimationController.value = 0.0; + unawaited(_positionAnimationController.fling(velocity: 0.4)); } void animateRotation(double from, double to) { @@ -260,9 +260,8 @@ class PhotoViewCoreState extends State return; } _rotationAnimation = Tween(begin: from, end: to).animate(_rotationAnimationController); - _rotationAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _rotationAnimationController.value = 0.0; + unawaited(_rotationAnimationController.fling(velocity: 0.4)); } void onAnimationStatus(AnimationStatus status) { @@ -436,7 +435,7 @@ class PhotoViewCoreState extends State ? SizedBox( width: scaleBoundaries.childSize.width * scale, height: scaleBoundaries.childSize.height * scale, - child: widget.customChild!, + child: widget.customChild, ) : Image( key: widget.heroAttributes?.tag != null ? ObjectKey(widget.heroAttributes!.tag) : null, diff --git a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart index a9cfeb3a40..e55df57105 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -75,14 +75,13 @@ class ImageWrapper extends StatefulWidget { final int index; @override - createState() => _ImageWrapperState(); + State createState() => _ImageWrapperState(); } class _ImageWrapperState extends State { ImageStreamListener? _imageStreamListener; ImageStream? _imageStream; ImageChunkEvent? _loadingProgress; - ImageInfo? _imageInfo; bool _loading = true; Size? _imageSize; Object? _lastException; @@ -122,7 +121,7 @@ class _ImageWrapperState extends State { // retrieve image from the provider void _resolveImage() { - final ImageStream newStream = widget.imageProvider.resolve(const ImageConfiguration()); + final ImageStream newStream = widget.imageProvider.resolve(ImageConfiguration.empty); _updateSourceStream(newStream); } @@ -135,10 +134,9 @@ class _ImageWrapperState extends State { } void handleImageFrame(ImageInfo info, bool synchronousCall) { - setupCB() { + void setupCB() { _imageSize = Size(info.image.width.toDouble(), info.image.height.toDouble()); _loading = false; - _imageInfo = _imageInfo; _loadingProgress = null; _lastException = null; 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 dee42ec5a0..1c338b8828 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 @@ -20,7 +20,7 @@ class FilterBottomSheetScaffold extends StatelessWidget { @override Widget build(BuildContext context) { - buildChildWidget() { + Widget buildChildWidget() { if (expanded != null && expanded == true) { return Expanded(child: child); } diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index a9382ec3ae..0732b30a63 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; @@ -21,7 +21,7 @@ class PeoplePicker extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final formFocus = useFocusNode(); - final imageSize = 60.0; + const imageSize = 60.0; final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); final selectedPeople = useState>(filter ?? {}); 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 32d1ab5bd4..864ddf2591 100644 --- a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart +++ b/mobile/lib/widgets/search/search_filter/star_rating_picker.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('rating_count'.t(args: {'count': index})), value: index, ), ), diff --git a/mobile/lib/widgets/search/thumbnail_with_info.dart b/mobile/lib/widgets/search/thumbnail_with_info.dart index 7ba8257c8a..d25a324860 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info.dart @@ -22,7 +22,7 @@ class ThumbnailWithInfo extends StatelessWidget { @override Widget build(BuildContext context) { - var textAndIconColor = context.isDarkTheme ? Colors.grey[100] : Colors.grey[700]; + final textAndIconColor = context.isDarkTheme ? Colors.grey[100] : Colors.grey[700]; return ThumbnailWithInfoContainer( onTap: onTap, borderRadius: borderRadius, diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 542a7cc5e2..cb035a22e9 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -7,9 +8,9 @@ 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/providers/infrastructure/settings.provider.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'; import 'package:immich_mobile/repositories/permission.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; @@ -31,35 +32,38 @@ class AdvancedSettings extends HookConsumerWidget { final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); final isManageMediaSupported = useState(false); final manageMediaAndroidPermission = useState(false); - final levelId = useState(ref.read(appConfigProvider).logLevel.index); - final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote); + final levelId = useState(ref.watch(appConfigProvider).logLevel.index); + final preferRemote = useState(ref.watch(appConfigProvider).image.preferRemote); useValueChanged( preferRemote.value, - (_, __) => ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value), + (_, __) => unawaited(ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value)), ); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; - useValueChanged(levelId.value, (_, __) => LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel())); + useValueChanged( + levelId.value, + (_, __) => unawaited(LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel())), + ); Future checkAndroidVersion() async { if (Platform.isAndroid) { - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - int sdkVersion = androidInfo.version.sdkInt; + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + final int sdkVersion = androidInfo.version.sdkInt; return sdkVersion >= 31; } return false; } useEffect(() { - () async { + unawaited(() async { isManageMediaSupported.value = await checkAndroidVersion(); - if (isManageMediaSupported.value) { + if (isManageMediaSupported.value && context.mounted) { manageMediaAndroidPermission.value = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); } - }(); + }()); return null; }, []); @@ -141,6 +145,10 @@ class AdvancedSettings extends HookConsumerWidget { try { clearedBytes = await remoteImageApi.clearCache(); } catch (e) { + if (!context.mounted) { + return; + } + context.scaffoldMessenger.showSnackBar( SnackBar( duration: const Duration(seconds: 2), @@ -153,7 +161,7 @@ class AdvancedSettings extends HookConsumerWidget { return; } - if (clearedBytes < 0) { + if (clearedBytes < 0 || !context.mounted) { return; } 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 f915df04f8..5b4c4bb89f 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,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -13,9 +15,9 @@ class LayoutSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tilesPerRow = useState(ref.read(appConfigProvider.select((s) => s.timeline.tilesPerRow))); + final tilesPerRow = useState(ref.watch(appConfigProvider.select((s) => s.timeline.tilesPerRow))); useValueChanged(tilesPerRow.value, (_, __) { - ref.read(settingsProvider).write(.timelineTilesPerRow, tilesPerRow.value); + unawaited(ref.read(settingsProvider).write(.timelineTilesPerRow, tilesPerRow.value)); }); return Column( 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 3ac72d6612..1f3f4bbcf0 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,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,7 +23,7 @@ class AssetListSettings extends HookConsumerWidget { valueNotifier: storageIndicator, title: 'theme_setting_asset_list_storage_indicator_title'.tr(), onChanged: (value) { - ref.read(settingsProvider).write(.timelineStorageIndicator, value); + unawaited(ref.read(settingsProvider).write(.timelineStorageIndicator, value)); ref.invalidate(appSettingsServiceProvider); ref.invalidate(settingsProvider); }, diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart index f3b9039b2b..25829327e0 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart'; -import 'package:immich_mobile/widgets/settings/asset_viewer_settings/video_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/slideshow_settings.dart'; +import 'package:immich_mobile/widgets/settings/asset_viewer_settings/video_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; class AssetViewerSettings extends StatelessWidget { 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 f65af6af9d..82a2d804a3 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -12,9 +14,9 @@ class ImageViewerQualitySetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal); + final isOriginal = useState(ref.watch(appConfigProvider).image.loadOriginal); useValueChanged(isOriginal.value, (_, __) { - ref.read(settingsProvider).write(.imageLoadOriginal, isOriginal.value); + unawaited(ref.read(settingsProvider).write(.imageLoadOriginal, isOriginal.value)); }); return Column( 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 730521e3c1..fc3466c3c6 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,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -11,9 +13,9 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tapToNavigate = useState(ref.read(appConfigProvider).viewer.tapToNavigate); + final tapToNavigate = useState(ref.watch(appConfigProvider).viewer.tapToNavigate); useValueChanged(tapToNavigate.value, (_, __) { - ref.read(settingsProvider).write(.viewerTapToNavigate, tapToNavigate.value); + unawaited(ref.read(settingsProvider).write(.viewerTapToNavigate, tapToNavigate.value)); }); return Column( 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 af361943ec..d8ccd4f0ed 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -15,23 +17,23 @@ class SlideshowSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final slideshow = ref.read(appConfigProvider).slideshow; + final slideshow = ref.watch(appConfigProvider).slideshow; final useRepeat = useState(slideshow.repeat); final useDuration = useState(slideshow.duration); final useLook = useState(slideshow.look); final useDirection = useState(slideshow.direction); useValueChanged(useRepeat.value, (_, __) { - ref.read(settingsProvider).write(.slideshowRepeat, useRepeat.value); + unawaited(ref.read(settingsProvider).write(.slideshowRepeat, useRepeat.value)); }); useValueChanged(useDuration.value, (_, __) { - ref.read(settingsProvider).write(.slideshowDuration, useDuration.value); + unawaited(ref.read(settingsProvider).write(.slideshowDuration, useDuration.value)); }); useValueChanged(useLook.value, (_, __) { - ref.read(settingsProvider).write(.slideshowLook, useLook.value); + unawaited(ref.read(settingsProvider).write(.slideshowLook, useLook.value)); }); useValueChanged(useDirection.value, (_, __) { - ref.read(settingsProvider).write(.slideshowDirection, useDirection.value); + unawaited(ref.read(settingsProvider).write(.slideshowDirection, useDirection.value)); }); return Column( 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 81929d95b9..7bea439f0c 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -11,19 +13,19 @@ class VideoViewerSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final viewer = ref.read(appConfigProvider).viewer; + final viewer = ref.watch(appConfigProvider).viewer; final useAutoPlayVideo = useState(viewer.autoPlayVideo); final useLoopVideo = useState(viewer.loopVideo); final useOriginalVideo = useState(viewer.loadOriginalVideo); useValueChanged(useAutoPlayVideo.value, (_, __) { - ref.read(settingsProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value); + unawaited(ref.read(settingsProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value)); }); useValueChanged(useLoopVideo.value, (_, __) { - ref.read(settingsProvider).write(.viewerLoopVideo, useLoopVideo.value); + unawaited(ref.read(settingsProvider).write(.viewerLoopVideo, useLoopVideo.value)); }); useValueChanged(useOriginalVideo.value, (_, __) { - ref.read(settingsProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value); + unawaited(ref.read(settingsProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value)); }); return Column( 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 44a1a1d7a9..e7d56ed66a 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -19,11 +19,11 @@ import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -class DriftBackupSettings extends ConsumerWidget { +class DriftBackupSettings extends StatelessWidget { const DriftBackupSettings({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return SettingsSubPageScaffold( settings: [ SettingGroupTitle( @@ -68,8 +68,9 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> }); try { - await ref.read(backgroundSyncProvider).syncLinkedAlbum(); - await ref.read(backgroundSyncProvider).syncRemote(); + final backgroundSync = ref.read(backgroundSyncProvider); + await backgroundSync.syncLinkedAlbum(); + await backgroundSync.syncRemote(); } catch (_) { } finally { Future.delayed(const Duration(seconds: 1), () { @@ -225,14 +226,14 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final fgService = ref.read(backgroundWorkerFgServiceProvider); + final fgService = ref.watch(backgroundWorkerFgServiceProvider); return _BackupSwitchTile( metadataKey: SettingsKey.backupRequireCharging, selector: (c) => c.backup.requireCharging, titleKey: "charging", subtitleKey: "charging_requirement_mobile_backup", onChanged: (value) { - fgService.configure(requireCharging: 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 92787077a1..097f72e1e1 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 @@ -39,12 +39,15 @@ class SyncStatusAndActions extends HookConsumerWidget { final documentsDir = await getApplicationDocumentsDirectory(); final dbFile = File(path.join(documentsDir.path, 'immich.sqlite')); + // ignore: avoid_slow_async_io if (!await dbFile.exists()) { - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database file not found".t(context: context))), - ); + if (!context.mounted) { + return; } + + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Database file not found".t(context: context))), + ); return; } @@ -53,6 +56,10 @@ class SyncStatusAndActions extends HookConsumerWidget { await dbFile.copy(exportFile.path); + if (!context.mounted) { + return; + } + final size = MediaQuery.of(context).size; await Share.shareXFiles( [XFile(exportFile.path)], @@ -61,22 +68,26 @@ class SyncStatusAndActions extends HookConsumerWidget { ); Future.delayed(const Duration(seconds: 30), () async { + // ignore: avoid_slow_async_io if (await exportFile.exists()) { await exportFile.delete(); } }); + if (!context.mounted) { + return; + } - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database exported successfully".t(context: context))), - ); - } + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Database exported successfully".t(context: context))), + ); } catch (e) { - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Failed to export database: $e".t(context: context))), - ); + if (!context.mounted) { + return; } + + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Failed to export database: $e".t(context: context))), + ); } } @@ -96,6 +107,10 @@ class SyncStatusAndActions extends HookConsumerWidget { TextButton( onPressed: () async { await ref.read(driftProvider).reset(); + if (!context.mounted) { + return; + } + context.pop(); unawaited( showDialog( @@ -130,7 +145,7 @@ class SyncStatusAndActions extends HookConsumerWidget { leading: const Icon(Icons.sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus), onTap: () { - ref.read(backgroundSyncProvider).syncLocal(full: true); + unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true)); }, ), SettingListTile( @@ -139,7 +154,7 @@ class SyncStatusAndActions extends HookConsumerWidget { leading: const Icon(Icons.cloud_sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus), onTap: () { - ref.read(backgroundSyncProvider).syncRemote(); + unawaited(ref.read(backgroundSyncProvider).syncRemote()); }, ), if (CurrentPlatform.isIOS && serverVersion.isAtLeast(major: 2, minor: 5)) @@ -148,7 +163,7 @@ class SyncStatusAndActions extends HookConsumerWidget { leading: const Icon(Icons.cloud_circle_rounded), subtitle: "tap_to_run_job".t(context: context), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).cloudIdSyncStatus), - onTap: ref.read(backgroundSyncProvider).syncCloudIds, + onTap: ref.watch(backgroundSyncProvider).syncCloudIds, ), SettingListTile( title: "hash_asset".t(context: context), @@ -156,7 +171,7 @@ class SyncStatusAndActions extends HookConsumerWidget { subtitle: "tap_to_run_job".t(context: context), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus), onTap: () { - ref.read(backgroundSyncProvider).hashAssets(); + unawaited(ref.read(backgroundSyncProvider).hashAssets()); }, ), const Divider(height: 1), diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index da14933997..8d4785d664 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -30,18 +32,18 @@ class _FreeUpSpaceSettingsState extends ConsumerState { @override void initState() { super.initState(); - WakelockPlus.enable(); + unawaited(WakelockPlus.enable()); WidgetsBinding.instance.addPostFrameCallback((_) { - _initializeAlbumDefaults(); + unawaited(_initializeAlbumDefaults()); }); } Future _initializeAlbumDefaults() async { + final notifier = ref.read(cleanupProvider.notifier); final albums = await ref.read(localAlbumProvider.future); final existingAlbumIds = albums.map((a) => a.id).toSet(); final albumsWithNames = albums.map((a) => (a.id, a.name)).toList(); - final notifier = ref.read(cleanupProvider.notifier); notifier.applyDefaultAlbumSelections(albumsWithNames); notifier.cleanupStaleAlbumIds(existingAlbumIds); } @@ -68,7 +70,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { void _goToScanStep() { ref.read(hapticFeedbackProvider.notifier).mediumImpact(); setState(() => _currentStep = CleanupStep.scan); - _scanAssets(); + unawaited(_scanAssets()); } void _setPresetDate(int daysAgo) { @@ -103,7 +105,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { lastDate: DateTime.now(), ); - if (picked != null) { + if (picked != null && context.mounted) { ref.read(cleanupProvider.notifier).setSelectedDate(picked); setState(() => _hasScanned = false); } @@ -120,6 +122,10 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ref.read(hapticFeedbackProvider.notifier).mediumImpact(); await ref.read(cleanupProvider.notifier).scanAssets(); + if (!mounted) { + return; + } + final state = ref.read(cleanupProvider); setState(() { @@ -144,7 +150,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { _DeleteConfirmationDialog(assetCount: state.assetsToDelete.length, cutoffDate: state.selectedDate!), ); - if (confirmed != true) { + if (confirmed != true || !context.mounted) { return; } @@ -169,13 +175,13 @@ class _FreeUpSpaceSettingsState extends ConsumerState { void _showAssetsPreview(List assets) { ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - context.pushRoute(CleanupPreviewRoute(assets: assets)); + unawaited(context.pushRoute(CleanupPreviewRoute(assets: assets))); } @override - dispose() { + void dispose() { super.dispose(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); } @override @@ -773,7 +779,7 @@ class _DatePresetCard extends StatelessWidget { child: InkWell( onTap: onTap, borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(12)), border: Border.all(color: isSelected ? context.colorScheme.primary : Colors.transparent, width: 1), diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 2482801923..e8478da453 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -3,14 +3,13 @@ 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/constants/locales.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; -class LanguageSettings extends HookConsumerWidget { +class LanguageSettings extends HookWidget { const LanguageSettings({super.key}); Future _applyLanguageChange( @@ -21,6 +20,10 @@ class LanguageSettings extends HookConsumerWidget { isLoading.value = true; await Future.delayed(const Duration(milliseconds: 500)); try { + if (!context.mounted) { + return; + } + await context.setLocale(selectedLocale.value); await loadTranslations(); } finally { @@ -29,7 +32,7 @@ class LanguageSettings extends HookConsumerWidget { } @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final localeEntries = useMemoized(() => locales.entries.toList(), const []); final currentLocale = context.locale; final filteredLocaleEntries = useState>>(localeEntries); diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index e8310caed4..162f1252a8 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -53,7 +55,7 @@ class EndpointInputState extends ConsumerState { void _onOutFocus() { if (!focusNode.hasFocus && isInputValid) { - validateAuxilaryServerUrl(); + unawaited(validateAuxilaryServerUrl()); } } 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 8250ef438a..81ffb4b19b 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -19,7 +21,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { final entries = useState([const AuxilaryEndpoint(url: '', status: AuxCheckStatus.unknown)]); final canSave = useState(false); - saveEndpointList() { + Future saveEndpointList() { canSave.value = entries.value.every((e) => e.status == AuxCheckStatus.valid); final urls = entries.value @@ -30,27 +32,27 @@ class ExternalNetworkPreference extends HookConsumerWidget { return ref.read(settingsProvider).write(SettingsKey.networkExternalEndpointList, urls); } - updateValidationStatus(String url, int index, AuxCheckStatus status) async { + Future updateValidationStatus(String url, int index, AuxCheckStatus status) async { entries.value[index] = entries.value[index].copyWith(url: url, status: status); await saveEndpointList(); - if (status == AuxCheckStatus.valid) { + if (status == AuxCheckStatus.valid && context.mounted) { await ref.read(apiServiceProvider).updateHeaders(); } } - handleReorder(int oldIndex, int newIndex) { + void handleReorder(int oldIndex, int newIndex) { final entry = entries.value.removeAt(oldIndex); entries.value.insert(newIndex, entry); entries.value = [...entries.value]; - saveEndpointList(); + unawaited(saveEndpointList()); } - handleDismiss(int index) { + void handleDismiss(int index) { entries.value = [...entries.value..removeAt(index)]; - saveEndpointList(); + unawaited(saveEndpointList()); } Widget proxyDecorator(Widget child, int _, Animation animation) { 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 1d2b5eea0f..8b787bcaea 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -62,17 +62,17 @@ class LocalNetworkPreference extends HookConsumerWidget { return null; }, []); - saveWifiName(String wifiName) { + Future saveWifiName(String wifiName) { wifiNameText.value = wifiName; return ref.read(authProvider.notifier).saveWifiName(wifiName); } - saveLocalEndpoint(String url) { + Future saveLocalEndpoint(String url) { localEndpointText.value = url; return ref.read(authProvider.notifier).saveLocalEndpoint(url); } - handleEditWifiName() async { + Future handleEditWifiName() async { final wifiName = await _showEditDialog(context, "wifi_name".tr(), "your_wifi_name".tr(), wifiNameText.value); if (wifiName != null) { @@ -80,7 +80,7 @@ class LocalNetworkPreference extends HookConsumerWidget { } } - handleEditServerEndpoint() async { + Future handleEditServerEndpoint() async { final localEndpoint = await _showEditDialog( context, "server_endpoint".tr(), @@ -94,8 +94,11 @@ class LocalNetworkPreference extends HookConsumerWidget { } } - autofillCurrentNetwork() async { + Future autofillCurrentNetwork() async { final wifiName = await ref.read(networkProvider.notifier).getWifiName(); + if (!context.mounted) { + return; + } if (wifiName == null) { context.showSnackBar( diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index 7e6e169de7..dad827230a 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -19,9 +21,9 @@ class NetworkingSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currentEndpoint = getServerUrl(); - final featureEnabled = useState(ref.read(appConfigProvider).network.autoEndpointSwitching); + final featureEnabled = useState(ref.watch(appConfigProvider).network.autoEndpointSwitching); useValueChanged(featureEnabled.value, (_, __) { - ref.read(settingsProvider).write(.networkAutoEndpointSwitching, featureEnabled.value); + unawaited(ref.read(settingsProvider).write(.networkAutoEndpointSwitching, featureEnabled.value)); }); Future checkWifiReadPermission() async { @@ -44,6 +46,10 @@ class NetworkingSettings extends HookConsumerWidget { onPressed: () async { final isGrant = await ref.read(networkProvider.notifier).requestWifiReadPermission(); + if (!context.mounted) { + return; + } + Navigator.pop(context, isGrant); }, child: Text("grant_permission".tr()), @@ -54,6 +60,10 @@ class NetworkingSettings extends HookConsumerWidget { ); } + if (!context.mounted) { + return; + } + if (!hasLocationAlways) { isGrantLocationAlwaysPermission = await showDialog( context: context, @@ -66,6 +76,10 @@ class NetworkingSettings extends HookConsumerWidget { onPressed: () async { final isGrant = await ref.read(networkProvider.notifier).requestWifiReadBackgroundPermission(); + if (!context.mounted) { + return; + } + Navigator.pop(context, isGrant); }, child: Text("grant_permission".tr()), @@ -83,7 +97,7 @@ class NetworkingSettings extends HookConsumerWidget { useEffect(() { if (featureEnabled.value == true) { - checkWifiReadPermission(); + unawaited(checkWifiReadPermission()); } return null; }, [featureEnabled.value]); diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart index 8b7c652925..0bab4e051a 100644 --- a/mobile/lib/widgets/settings/notification_setting.dart +++ b/mobile/lib/widgets/settings/notification_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -15,22 +17,24 @@ class NotificationSetting extends HookConsumerWidget { final permissionService = ref.watch(notificationPermissionProvider); final hasPermission = permissionService == PermissionStatus.granted; - openAppNotificationSettings(BuildContext ctx) { + void openAppNotificationSettings(BuildContext ctx) { ctx.pop(); - openAppSettings(); + unawaited(openAppSettings()); } // When permissions are permanently denied, you need to go to settings to // allow them - showPermissionsDialog() { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - content: const Text('notification_permission_dialog_content').tr(), - actions: [ - TextButton(child: const Text('cancel').tr(), onPressed: () => ctx.pop()), - TextButton(onPressed: () => openAppNotificationSettings(ctx), child: const Text('settings').tr()), - ], + void showPermissionsDialog() { + unawaited( + showDialog( + context: context, + builder: (ctx) => AlertDialog( + content: const Text('notification_permission_dialog_content').tr(), + actions: [ + TextButton(child: const Text('cancel').tr(), onPressed: () => ctx.pop()), + TextButton(onPressed: () => openAppNotificationSettings(ctx), child: const Text('settings').tr()), + ], + ), ), ); } @@ -43,7 +47,7 @@ class NotificationSetting extends HookConsumerWidget { subtileText: 'notification_permission_list_tile_content'.tr(), buttonText: 'notification_permission_list_tile_enable_button'.tr(), onButtonTap: () => - ref.watch(notificationPermissionProvider.notifier).requestNotificationPermission().then((permission) { + ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((permission) { if (permission == PermissionStatus.permanentlyDenied) { showPermissionsDialog(); } diff --git a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart index 5e745dd61d..c5003a9ced 100644 --- a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart @@ -1,21 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -class HapticSetting extends HookConsumerWidget { +class HapticSetting extends HookWidget { const HapticSetting({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final hapticFeedbackSetting = useAppSettingsState(AppSettingsEnum.enableHapticFeedback); final isHapticFeedbackEnabled = useValueNotifier(hapticFeedbackSetting.value); - onHapticFeedbackChange(bool isEnabled) { + void onHapticFeedbackChange(bool isEnabled) { hapticFeedbackSetting.value = isEnabled; } diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 48d0ca672b..7964ec3861 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,33 +16,37 @@ class PrimaryColorSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeProvider = ref.read(immichThemeProvider); + final themeProvider = ref.watch(immichThemeProvider); final themeConfig = ref.watch(appConfigProvider.select((config) => config.theme)); const tileSize = 55.0; void popBottomSheet() { Future.delayed(const Duration(milliseconds: 200), () { + if (!context.mounted) { + return; + } + Navigator.pop(context); }); } - onUseSystemColorChange(bool newValue) { - ref.read(settingsProvider).write(.themeDynamic, newValue); + void onUseSystemColorChange(bool newValue) { + unawaited(ref.read(settingsProvider).write(.themeDynamic, newValue)); popBottomSheet(); } - onPrimaryColorChange(ImmichColorPreset colorPreset) { - ref.read(settingsProvider).write(.themePrimaryColor, colorPreset); + void onPrimaryColorChange(ImmichColorPreset colorPreset) { + unawaited(ref.read(settingsProvider).write(.themePrimaryColor, colorPreset)); //turn off system color setting if (themeConfig.dynamicTheme) { - ref.read(settingsProvider).write(.themeDynamic, false); + unawaited(ref.read(settingsProvider).write(.themeDynamic, false)); } popBottomSheet(); } - buildPrimaryColorTile({ + Container buildPrimaryColorTile({ required Color topColor, required Color bottomColor, required double tileSize, @@ -69,7 +75,7 @@ class PrimaryColorSetting extends HookConsumerWidget { right: 0, top: 0, bottom: 0, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(100)), color: Colors.grey[900]?.withValues(alpha: .4), @@ -85,7 +91,7 @@ class PrimaryColorSetting extends HookConsumerWidget { ); } - bottomSheetContent() { + Column bottomSheetContent() { return Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/mobile/lib/widgets/settings/preference_settings/share_setting.dart b/mobile/lib/widgets/settings/preference_settings/share_setting.dart index 2435810566..1881703023 100644 --- a/mobile/lib/widgets/settings/preference_settings/share_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/share_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -19,7 +21,7 @@ class ShareSetting extends HookConsumerWidget { void onChanged(ShareAssetType? value) { if (value != null) { fileType.value = value; - ref.read(settingsProvider).write(SettingsKey.shareFileType, value); + unawaited(ref.read(settingsProvider).write(SettingsKey.shareFileType, value)); } } diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index ffeeceae02..8281b2262a 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -13,7 +15,7 @@ class ThemeSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentTheme = useState(ref.read(appConfigProvider.select((config) => config.theme.mode))); + final currentTheme = useState(ref.watch(appConfigProvider.select((config) => config.theme.mode))); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); final colorfulInterface = useValueNotifier( @@ -22,7 +24,7 @@ class ThemeSetting extends HookConsumerWidget { void onThemeChange(bool isDark) { currentTheme.value = isDark ? ThemeMode.dark : ThemeMode.light; - ref.read(settingsProvider).write(.themeMode, currentTheme.value); + unawaited(ref.read(settingsProvider).write(.themeMode, currentTheme.value)); } void onSystemThemeChange(bool isSystem) { @@ -39,11 +41,11 @@ class ThemeSetting extends HookConsumerWidget { currentTheme.value = ThemeMode.dark; } } - ref.read(settingsProvider).write(.themeMode, currentTheme.value); + unawaited(ref.read(settingsProvider).write(.themeMode, currentTheme.value)); } void onSurfaceColorSettingChange(bool useColorfulInterface) { - ref.read(settingsProvider).write(.themeColorfulInterface, useColorfulInterface); + unawaited(ref.read(settingsProvider).write(.themeColorfulInterface, useColorfulInterface)); colorfulInterface.value = useColorfulInterface; } diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index d8ed3ac017..4d358fa2b6 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -44,7 +44,7 @@ class SettingsSwitchListTile extends StatelessWidget { onChanged: onSwitchChanged, activeThumbColor: enabled ? context.primaryColor : context.themeData.disabledColor, dense: true, - secondary: icon != null ? Icon(icon!, color: valueNotifier.value ? context.primaryColor : null) : null, + secondary: icon != null ? Icon(icon, color: valueNotifier.value ? context.primaryColor : null) : null, title: Text( title, style: diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index d419d6ead0..d98925bdf8 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -179,7 +179,7 @@ class SharedLinkItem extends ConsumerWidget { ), ); - if (confirmed == true) { + if (confirmed == true && context.mounted) { await ref.read(sharedLinksStateProvider.notifier).deleteLink(sharedLink.id); return true; } diff --git a/mobile/lib/wm_executor.dart b/mobile/lib/wm_executor.dart index e873c5f76d..aac6d3e174 100644 --- a/mobile/lib/wm_executor.dart +++ b/mobile/lib/wm_executor.dart @@ -145,7 +145,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { void _schedule() { final availableWorker = _pool.firstWhereOrNull((worker) => worker.taskId == null && worker.initialized); if (availableWorker == null) { - _ensureWorkersInitialized(); + unawaited(_ensureWorkersInitialized()); return; } if (_queue.isEmpty) { @@ -153,26 +153,28 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { } final task = _queue.removeFirst(); - availableWorker - .work(task) - .then( - (value) { - //might be completed by cancel and it is normal. - //Assuming that worker finished with error and cleaned gracefully - task.complete(value, null, null); - }, - onError: (error, st) { - task.complete(null, error, st); - }, - ) - .whenComplete(() { - if (_dynamicSpawning && _queue.isEmpty) { - // Retire the idle worker; shutdown() nulls its fields so the husk - // stays pooled and is revived by initialize() if work arrives. - unawaited(availableWorker.shutdown()); - } - _schedule(); - }); + unawaited( + availableWorker + .work(task) + .then( + (value) { + //might be completed by cancel and it is normal. + //Assuming that worker finished with error and cleaned gracefully + task.complete(value, null, null); + }, + onError: (error, st) { + task.complete(null, error, st); + }, + ) + .whenComplete(() { + if (_dynamicSpawning && _queue.isEmpty) { + // Retire the idle worker; shutdown() nulls its fields so the husk + // stays pooled and is revived by initialize() if work arrives. + unawaited(availableWorker.shutdown()); + } + _schedule(); + }), + ); } @override diff --git a/mobile/mise.toml b/mobile/mise.toml index ce7c626e18..7caf6dfbd5 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -17,7 +17,7 @@ windows-x64 = { asset_pattern = "dcm-windows-release.zip" } [tasks.codegen] alias = "codegen" description = "Generate all codegen artifacts" -depends = ["codegen:dart", "codegen:drift:schema", "codegen:pigeon", "codegen:translation"] +depends = ["//:open-api-dart", "codegen:dart", "codegen:drift:schema", "codegen:pigeon", "codegen:translation"] [tasks."codegen:dart"] description = "Execute build_runner to auto-generate dart code" @@ -29,20 +29,22 @@ sources = [ ] outputs = { auto = true } run = [ - "dart run build_runner build --delete-conflicting-outputs", + "dart run build_runner build", "dart format lib/routing/router.gr.dart", ] +depends = ["//:open-api-dart"] [tasks."codegen:drift:schema"] description = "Generate Drift migration schema test code" sources = ["drift_schemas/main/*.json"] outputs = { auto = true } run = "dart run drift_dev schema generate --data-classes --companions drift_schemas/main/ test/drift/main/generated/" +depends = ["//:open-api-dart"] [tasks."codegen:watch"] alias = "watch" description = "Watch and auto-generate dart code" -run = "dart run build_runner watch --delete-conflicting-outputs" +run = "dart run build_runner watch" [tasks."codegen:pigeon"] alias = "pigeon" @@ -51,17 +53,12 @@ run = [ "ls pigeon/*.dart | xargs -n1 -P4 -I{} dart run pigeon --input {}", "dart format lib/platform/", ] +depends = ["//:open-api-dart"] [tasks."codegen:translation"] alias = "translation" description = "Generate translations from i18n JSONs" -run = [ - { task = "//:i18n:format-fix" }, - { tasks = [ - "i18n:loader", - "i18n:keys", - ] }, -] +depends = ["//:i18n:format-fix", "i18n:loader", "i18n:keys"] [tasks."codegen:app-icon"] description = "Generate app icons" @@ -123,6 +120,8 @@ run = [ "dart run easy_localization:generate -S ../i18n", "dart format lib/generated/codegen_loader.g.dart", ] +depends = ["//:open-api-dart"] +wait_for = ["//:i18n:format-fix"] [tasks."i18n:keys"] description = "Generate i18n keys" @@ -131,6 +130,7 @@ run = [ "dart run bin/generate_keys.dart", "dart format lib/generated/translations.g.dart", ] +wait_for = ["//:i18n:format-fix"] [tasks."analyze:dart"] description = "Run Dart analysis" diff --git a/mobile/packages/ui/lib/src/components/column_button.dart b/mobile/packages/ui/lib/src/components/column_button.dart index f990dbc065..03b6933389 100644 --- a/mobile/packages/ui/lib/src/components/column_button.dart +++ b/mobile/packages/ui/lib/src/components/column_button.dart @@ -43,21 +43,10 @@ class _ImmichColumnButtonState extends State { } } - Future? _onPressed() { - if (_isDisabled) { - return null; - } + VoidCallback? get _onPressed => _isDisabled ? null : () => _runAction(widget.onPressed); - return _runAction(widget.onPressed); - } - - Future? _onLongPress() { - if (_isDisabled || widget.onLongPress == null) { - return null; - } - - return _runAction(widget.onLongPress!); - } + VoidCallback? get _onLongPress => + _isDisabled || widget.onLongPress == null ? null : () => _runAction(widget.onLongPress!); @override Widget build(BuildContext context) { diff --git a/mobile/packages/ui/lib/src/components/icon_button.dart b/mobile/packages/ui/lib/src/components/icon_button.dart index 1c02f30ed3..c0e1bcc3fa 100644 --- a/mobile/packages/ui/lib/src/components/icon_button.dart +++ b/mobile/packages/ui/lib/src/components/icon_button.dart @@ -44,21 +44,10 @@ class _ImmichIconButtonState extends State { } } - Future? _onPressed() { - if (_isDisabled) { - return null; - } + VoidCallback? get _onPressed => _isDisabled ? null : () => _runAction(widget.onPressed); - return _runAction(widget.onPressed); - } - - Future? _onLongPress() { - if (_isDisabled || widget.onLongPress == null) { - return null; - } - - return _runAction(widget.onLongPress!); - } + VoidCallback? get _onLongPress => + _isDisabled || widget.onLongPress == null ? null : () => _runAction(widget.onLongPress!); @override Widget build(BuildContext context) { diff --git a/mobile/packages/ui/lib/src/components/password_input.dart b/mobile/packages/ui/lib/src/components/password_input.dart index e99e9730a8..d08b3de99d 100644 --- a/mobile/packages/ui/lib/src/components/password_input.dart +++ b/mobile/packages/ui/lib/src/components/password_input.dart @@ -51,7 +51,7 @@ class _ImmichPasswordInputState extends State { onPressed: _toggleVisibility, icon: Icon(_visible ? Icons.visibility_off_rounded : Icons.visibility_rounded), ), - autofillHints: [AutofillHints.password], + autofillHints: const [AutofillHints.password], ); } } diff --git a/mobile/packages/ui/lib/src/components/text_button.dart b/mobile/packages/ui/lib/src/components/text_button.dart index bbcfe4a3a7..6cbc08473b 100644 --- a/mobile/packages/ui/lib/src/components/text_button.dart +++ b/mobile/packages/ui/lib/src/components/text_button.dart @@ -46,21 +46,10 @@ class _ImmichTextButtonState extends State { } } - Future? _onPressed() { - if (_isDisabled) { - return null; - } + VoidCallback? get _onPressed => _isDisabled ? null : () => _runAction(widget.onPressed); - return _runAction(widget.onPressed); - } - - Future? _onLongPress() { - if (_isDisabled || widget.onLongPress == null) { - return null; - } - - return _runAction(widget.onLongPress!); - } + VoidCallback? get _onLongPress => + _isDisabled || widget.onLongPress == null ? null : () => _runAction(widget.onLongPress!); @override Widget build(BuildContext context) { diff --git a/mobile/packages/ui/test/formatted_text_test.dart b/mobile/packages/ui/test/formatted_text_test.dart index c3901cd802..5f26822855 100644 --- a/mobile/packages/ui/test/formatted_text_test.dart +++ b/mobile/packages/ui/test/formatted_text_test.dart @@ -59,7 +59,7 @@ void main() { ); final text = tester.widget(find.byType(Text)); - final richText = text.textSpan as TextSpan; + final richText = text.textSpan! as TextSpan; expect(richText.style?.fontSize, 16); expect(richText.style?.color, Colors.purple); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index e0198f2683..f02df0b622 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -667,6 +667,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.14" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 + url: "https://pub.dev" + source: hosted + version: "3.2.5" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" frontend_server_client: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a55bcf515b..c2848e6dd8 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: url: https://github.com/mertalev/http ref: '549c24b0a4d3881a9a44b70f4873450d43c1c4af' # https://github.com/dart-lang/http/pull/1877 path: pkgs/ok_http/ + freezed_annotation: ^3.1.0 dev_dependencies: auto_route_generator: ^10.5.0 @@ -104,6 +105,7 @@ dev_dependencies: flutter_native_splash: ^2.4.7 flutter_test: sdk: flutter + freezed: ^3.2.5 integration_test: sdk: flutter mocktail: ^1.0.5 diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index e033229408..5cd4104f91 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -36,7 +36,6 @@ class _AbortCallbackWrapper { class _MockAbortCallbackWrapper extends Mock implements _AbortCallbackWrapper {} - void main() { late SyncStreamService sut; late SyncStreamRepository mockSyncStreamRepo; @@ -70,7 +69,7 @@ void main() { await db.close(); }); - successHandler(Invocation _) async => true; + Future successHandler(Invocation _) async => true; setUp(() async { mockSyncStreamRepo = MockSyncStreamRepository(); diff --git a/mobile/test/infrastructure/repositories/local_album_repository_test.dart b/mobile/test/infrastructure/repositories/local_album_repository_test.dart index bd4acca483..ada4281e2a 100644 --- a/mobile/test/infrastructure/repositories/local_album_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_album_repository_test.dart @@ -17,6 +17,10 @@ void main() { mediumFactory = MediumFactory(db); }); + tearDown(() async { + await db.close(); + }); + group('getAll', () { test('sorts albums by backupSelection & isIosSharedAlbum', () async { final localAlbumRepo = mediumFactory.getRepository(); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index d538b567bd..3a3e40cbae 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -37,7 +37,7 @@ void main() { late MockHttpClient mockHttpClient; late MockStreamedResponse mockStreamedResponse; late StreamController> responseStreamController; - late int testBatchSize = 3; + const int testBatchSize = 3; setUpAll(() async { final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 0688576682..355e128fc0 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -3,9 +3,9 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; diff --git a/mobile/test/medium/repositories/local_asset_repository_test.dart b/mobile/test/medium/repositories/local_asset_repository_test.dart index bc74728346..2376445d1a 100644 --- a/mobile/test/medium/repositories/local_asset_repository_test.dart +++ b/mobile/test/medium/repositories/local_asset_repository_test.dart @@ -19,6 +19,73 @@ void main() { await ctx.dispose(); }); + group('get', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + // Owner-scoped queries resolve the current user via authUserEntity. + await ctx.newAuthUser(id: userId); + }); + + test('allows the same checksum to exist for multiple owners (#29973)', () async { + const checksum = 'some-shared-checksum'; + final mine = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.id, local.id); + // We must explicitly get OUR asset, not the partner's + expect(result.remoteId, mine.id); + }); + + test('reports local-only when only a partner has a remote copy (#29973)', () async { + // The current user has NOT uploaded this file; only a partner owns an identical-checksum remote asset + const checksum = 'partner-only'; + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.remoteId, isNull); + expect(result.storage, AssetState.local); + }); + + test('allows the current user to have access to multiple remote rows for one checksum (#29973)', () async { + // A single user can have their own remote asset, a partner's remote asset, and a local asset all with the same checksum + const checksum = 'multi-library'; + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.id, local.id); + expect(result.remoteId, isNotNull); + }); + + test('attaches remoteId to local asset automatically in simple scenarios', () async { + const checksum = 'simple'; + final remote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.remoteId, remote.id); + expect(result.storage, AssetState.merged); + }); + }); + group('getRemovalCandidates', () { final cutoffDate = DateTime(2024, 1, 1); final beforeCutoff = DateTime(2023, 12, 31); @@ -456,7 +523,7 @@ void main() { test('does not update when longitude does not match', () async { final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); - final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: .fromNullable((-74.006))); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: .fromNullable(-74.006)); final localAsset = await ctx.newLocalAsset( checksumOption: const Option.none(), iCloudId: cloudIdAsset.cloudId, diff --git a/mobile/test/medium/repositories/remote_asset_repository_test.dart b/mobile/test/medium/repositories/remote_asset_repository_test.dart new file mode 100644 index 0000000000..f534355d99 --- /dev/null +++ b/mobile/test/medium/repositories/remote_asset_repository_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; + +import '../repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late RemoteAssetRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = RemoteAssetRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getByChecksum', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + }); + + test('returns all assets when a partner shares the checksum', () async { + const checksum = 'shared-partner-checksum'; + final mine = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final partner = await ctx.newUser(); + final theirs = await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + + final result = await sut.getAllDebugForChecksum(checksum); + final mineResult = result.firstWhere((asset) => asset.id == mine.id); + final theirResult = result.firstWhere((asset) => asset.id == theirs.id); + + expect(result, isNotEmpty); + expect(mineResult.id, mine.id); + expect(mineResult.ownerId, userId); + + expect(theirResult.id, theirs.id); + expect(theirResult.ownerId, partner.id); + }); + + test('returns partner asset only if there is no matching user asset', () async { + const checksum = 'partner-only'; + final partner = await ctx.newUser(); + final theirs = await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + + final result = await sut.getAllDebugForChecksum(checksum); + + expect(result.length, 1); + expect(result[0].id, theirs.id); + }); + + test('returns the current user\'s asset', () async { + const checksum = 'simple'; + final remote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + + final result = await sut.getAllDebugForChecksum(checksum); + + expect(result.length, 1); + expect(result[0].id, remote.id); + }); + }); +} diff --git a/mobile/test/medium/repositories/timeline_repository_test.dart b/mobile/test/medium/repositories/timeline_repository_test.dart index 94b3413c9b..502a65d45b 100644 --- a/mobile/test/medium/repositories/timeline_repository_test.dart +++ b/mobile/test/medium/repositories/timeline_repository_test.dart @@ -27,7 +27,7 @@ void main() { // Regression check for #23273: a LEFT OUTER JOIN on checksum would fan out and create duplicates // happens when same photo exists in multiple albums on device final user = await ctx.newUser(); - final checksum = 'yolo'; + const checksum = 'yolo'; final album = await ctx.newRemoteAlbum(ownerId: user.id); final remoteAsset = await ctx.newRemoteAsset(ownerId: user.id, checksum: checksum); await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: remoteAsset.id); @@ -102,4 +102,48 @@ void main() { expect(remote.localId, local.id); }); }); + + group('localAlbum assets', () { + late String userId; + late String otherUserId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + final other = await ctx.newUser(); + otherUserId = other.id; + }); + + test('does not duplicate assets when a partner shares the checksum', () async { + const checksum = 'shared-partner-checksum'; + final album = await ctx.newLocalAlbum(); + final local = await ctx.newLocalAsset(checksum: checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + final myRemote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + await ctx.newRemoteAsset(ownerId: otherUserId, checksum: checksum); + + final assets = await sut.localAlbum(album.id, .day).assetSource(0, 10); + + expect(assets, hasLength(1)); + final asset = assets.single as LocalAsset; + expect(asset.id, local.id); + // Must resolve the current user's remote id + expect(asset.remoteId, myRemote.id); + }); + + test('bucket count ignores a partner sharing the checksum', () async { + const checksum = 'shared-partner-checksum'; + final album = await ctx.newLocalAlbum(); + final local = await ctx.newLocalAsset(checksum: checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + await ctx.newRemoteAsset(ownerId: otherUserId, checksum: checksum); + + final buckets = await sut.localAlbum(album.id, .day).bucketSource().first; + + expect(buckets, hasLength(1)); + expect(buckets.single.assetCount, 1); + }); + }); } diff --git a/mobile/test/medium/repositories/trashed_local_asset_repository_test.dart b/mobile/test/medium/repositories/trashed_local_asset_repository_test.dart new file mode 100644 index 0000000000..0af09715c7 --- /dev/null +++ b/mobile/test/medium/repositories/trashed_local_asset_repository_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; + +import '../repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftTrashedLocalAssetRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = DriftTrashedLocalAssetRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getToRestore', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + }); + + test('does not restore based on a partner\'s live remote copy', () async { + const checksum = 'shared-partner-checksum'; + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final trashed = await ctx.newTrashedLocalAsset(albumId: album.id, checksum: checksum); + + // Current user's own remote copy is deleted; only a partner has an active copy. + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum, deletedAt: DateTime(2020, 1, 1)); + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + + final result = await sut.getToRestore(); + + final ids = result.map((a) => a.id); + expect(ids, isNot(contains(trashed.id))); + }); + + test('restores when the current user\'s own remote copy is live', () async { + const checksum = 'my-live-copy'; + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final trashed = await ctx.newTrashedLocalAsset(albumId: album.id, checksum: checksum); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + + final result = await sut.getToRestore(); + + final ids = result.map((a) => a.id); + expect(ids, contains(trashed.id)); + }); + }); + + group('getToTrash', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + }); + + Future addLocalAssetToBackupAlbum(String checksum) async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final local = await ctx.newLocalAsset(checksum: checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + return local.id; + } + + test('does not trash when only a partner\'s remote copy is deleted', () async { + const checksum = 'shared-partner-checksum'; + final localId = await addLocalAssetToBackupAlbum(checksum); + + // Current user's own remote copy is live but a partner deleted theirs + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum, deletedAt: DateTime(2020, 1, 1)); + + final result = await sut.getToTrash(); + + final ids = result.values.expand((assets) => assets).map((a) => a.id); + expect(ids, isNot(contains(localId))); + }); + + test('trashes when the current user\'s own remote copy is deleted', () async { + const checksum = 'my-deleted-copy'; + final localId = await addLocalAssetToBackupAlbum(checksum); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum, deletedAt: DateTime(2020, 1, 1)); + + final result = await sut.getToTrash(); + + final ids = result.values.expand((assets) => assets).map((a) => a.id); + expect(ids, contains(localId)); + }); + }); +} diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart index ed06774e82..09b8e2c7eb 100644 --- a/mobile/test/medium/repository_context.dart +++ b/mobile/test/medium/repository_context.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; @@ -18,6 +19,8 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity. import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/utils/option.dart'; @@ -72,6 +75,22 @@ class MediumRepositoryContext { ); } + /// Seeds a user into `authUserEntity` as the currently authenticated user + Future newAuthUser({String? id, String? email, bool? isAdmin, AvatarColor? avatarColor}) async { + id ??= TestUtils.uuid(); + return db + .into(db.authUserEntity) + .insertReturning( + AuthUserEntityCompanion( + id: .new(id), + email: .new(email ?? '$id@test.com'), + name: .new('user_$id'), + isAdmin: .new(isAdmin ?? false), + avatarColor: .new(avatarColor ?? TestUtils.randElement(AvatarColor.values)), + ), + ); + } + Future newPartner({required String sharedById, required String sharedWithId, bool? inTimeline}) { return db .into(db.partnerEntity) @@ -286,6 +305,33 @@ class MediumRepositoryContext { ); } + /// Seeds a trashed local asset into `trashedLocalAssetEntity` + Future newTrashedLocalAsset({ + String? id, + required String albumId, + String? checksum, + TrashOrigin? source, + AssetType? type, + DateTime? createdAt, + bool? isFavorite, + }) async { + id ??= TestUtils.uuid(); + return db + .into(db.trashedLocalAssetEntity) + .insertReturning( + TrashedLocalAssetEntityCompanion( + id: .new(id), + albumId: .new(albumId), + name: .new('trashed_$id.jpg'), + type: .new(type ?? .image), + checksum: .new(checksum), + source: .new(source ?? TrashOrigin.remoteSync), + isFavorite: .new(isFavorite ?? false), + createdAt: .new(TestUtils.date(createdAt)), + ), + ); + } + Future newLocalAlbum({ String? id, String? name, diff --git a/mobile/test/modules/utils/async_mutex_test.dart b/mobile/test/modules/utils/async_mutex_test.dart index 08cafeb307..10fdcbad5e 100644 --- a/mobile/test/modules/utils/async_mutex_test.dart +++ b/mobile/test/modules/utils/async_mutex_test.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/utils/async_mutex.dart'; void main() { group('Test AsyncMutex grouped', () { test('test ordered execution', () async { - AsyncMutex lock = AsyncMutex(); - List events = []; + final AsyncMutex lock = AsyncMutex(); + final List events = []; expect(0, lock.enqueued); unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(1)))); expect(1, lock.enqueued); diff --git a/mobile/test/modules/utils/datetime_helpers_test.dart b/mobile/test/modules/utils/datetime_helpers_test.dart index dfe83b4925..ca196224a0 100644 --- a/mobile/test/modules/utils/datetime_helpers_test.dart +++ b/mobile/test/modules/utils/datetime_helpers_test.dart @@ -10,38 +10,38 @@ void main() { test('returns null for value below minimum allowed range', () { // _minMillisecondsSinceEpoch = -62135596800000 - final seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed + const seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed final result = tryFromSecondsSinceEpoch(seconds); expect(result, isNull); }); test('returns null for value above maximum allowed range', () { // _maxMillisecondsSinceEpoch = 8640000000000000 - final seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed + const seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed final result = tryFromSecondsSinceEpoch(seconds); expect(result, isNull); }); test('returns correct DateTime for minimum allowed value', () { - final seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp + const seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(-62135596800000)); }); test('returns correct DateTime for maximum allowed value', () { - final seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp + const seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(8640000000000000)); }); test('returns correct DateTime for negative timestamp', () { - final seconds = -1577836800; // Dec 31, 1919 (pre-epoch) + const seconds = -1577836800; // Dec 31, 1919 (pre-epoch) final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(-1577836800 * 1000)); }); test('returns correct DateTime for zero timestamp', () { - final seconds = 0; // Jan 1, 1970 (epoch) + const seconds = 0; // Jan 1, 1970 (epoch) final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(0)); }); diff --git a/mobile/test/modules/utils/debouncer_test.dart b/mobile/test/modules/utils/debouncer_test.dart index 7aa13842d6..036c538f03 100644 --- a/mobile/test/modules/utils/debouncer_test.dart +++ b/mobile/test/modules/utils/debouncer_test.dart @@ -11,7 +11,7 @@ class _Counter { void main() { test('Executes the method after the interval', () async { - var counter = _Counter(); + final counter = _Counter(); final debouncer = Debouncer(interval: const Duration(milliseconds: 300)); debouncer.run(() => counter.increment()); expect(counter.count, 0); @@ -20,17 +20,17 @@ void main() { }); test('Executes the method immediately if zero interval', () async { - var counter = _Counter(); - final debouncer = Debouncer(interval: const Duration(milliseconds: 0)); + final counter = _Counter(); + final debouncer = Debouncer(interval: Duration.zero); debouncer.run(() => counter.increment()); // Even though it is supposed to be executed immediately, it is added to the async queue and so // we need this delay to make sure the actual debounced method is called - await Future.delayed(const Duration(milliseconds: 0)); + await Future.delayed(Duration.zero); expect(counter.count, 1); }); test('Delayes method execution after all the calls are completed', () async { - var counter = _Counter(); + final counter = _Counter(); final debouncer = Debouncer(interval: const Duration(milliseconds: 100)); debouncer.run(() => counter.increment()); debouncer.run(() => counter.increment()); diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart index 18ab07b3a9..15d42c34f2 100644 --- a/mobile/test/modules/utils/openapi_patching_test.dart +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:openapi/api.dart'; import 'package:immich_mobile/utils/openapi_patching.dart'; +import 'package:openapi/api.dart'; void main() { group('Test OpenApi Patching', () { @@ -26,7 +26,7 @@ void main() { }); test('addDefault', () { - dynamic value = jsonDecode(""" + final dynamic value = jsonDecode(""" { "download": { "archiveSize": 4294967296, @@ -47,7 +47,7 @@ void main() { }); test('addDefault with null', () { - dynamic value = jsonDecode(""" + final dynamic value = jsonDecode(""" { "download": { "archiveSize": 4294967296, diff --git a/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart b/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart index 14d876a654..3ec55bb515 100644 --- a/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart +++ b/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart @@ -39,6 +39,10 @@ void main() { context = await PresentationContext.create(); }); + tearDown(() async { + await context.dispose(); + }); + testWidgets('status bar icons are light while the asset viewer is open in light mode', (tester) async { // Emulate arriving from a light-themed page whose AppBar set dark status // bar icons (the state the viewer is opened from in light mode). diff --git a/mobile/test/presentation/widgets/images/local_image_provider_test.dart b/mobile/test/presentation/widgets/images/local_image_provider_test.dart new file mode 100644 index 0000000000..5b556f8a41 --- /dev/null +++ b/mobile/test/presentation/widgets/images/local_image_provider_test.dart @@ -0,0 +1,141 @@ +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; + +import '../../../unit/factories/local_asset_factory.dart'; + +class _StubCompleter extends ImageStreamCompleter {} + +void main() { + late ImageCache cache; + late int loads; + + ImageStreamCompleter load() { + loads++; + return _StubCompleter(); + } + + setUp(() { + cache = ImageCache(); + loads = 0; + }); + + group('LocalFullImageProvider.previewTargetSize', () { + const box = Size(1179, 2556); + const cases = <(String, Size, int?, int?, bool, Size)>[ + ('normal', box, 4032, 3024, true, box), + ('missing dimensions', box, null, null, true, box), + ('invalid dimensions', box, 1000, 0, true, box), + ('long final preview', box, 1000, 30000, true, Size(16384 / 30, 16384)), + ('long first preview', box, 30000, 1000, false, Size(2556, 2556 / 30)), + ('ultra-thin preview', box, 10, 50000, false, Size(1, 2556)), + ('small source', box, 50, 2000, true, Size(50, 2000)), + ('at limit', Size(16384, 100), 1000, 1000, true, Size(16384, 100)), + ('over limit', Size(16385, 100), 1000, 1000, true, Size(1000, 1000)), + ]; + + for (final (name, box, width, height, previewIsFinal, expected) in cases) { + test(name, () { + final actual = LocalFullImageProvider.previewTargetSize( + box.width, + box.height, + width, + height, + previewIsFinal: previewIsFinal, + ); + expect(actual.width, closeTo(expected.width, 1e-6)); + expect(actual.height, closeTo(expected.height, 1e-6)); + }); + } + }); + + group('LocalFullImageProvider equality', () { + LocalFullImageProvider make({int? width, int? height}) => LocalFullImageProvider( + id: 'a', + assetType: AssetType.image, + size: const Size(100, 200), + isAnimated: false, + width: width, + height: height, + ); + + test('uses dimensions in the cache key', () { + final a = make(width: 100, height: 200); + final b = make(width: 100, height: 200); + expect(a, b); + expect(a.hashCode, b.hashCode); + expect(a == make(width: 200, height: 100), isFalse); + }); + }); + + group('LocalThumbProvider caching', () { + test('editing on device re-renders the thumbnail', () { + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image, checksum: 'before'), load); + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image, checksum: 'after'), load); + + expect(loads, 2); + }); + + test('an unchanged thumbnail still comes from the cache', () { + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image, checksum: 'same'), load); + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image, checksum: 'same'), load); + + expect(loads, 1); + }); + + // The rehash clears the checksum before writing the new one, so the tile has to + // follow that step too or it waits for the hash to land before showing the edit. + test('re-renders while the checksum is still being recomputed', () { + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image, checksum: 'before'), load); + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image), load); + + expect(loads, 2); + }); + + test('stays cached while the checksum is missing', () { + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image), load); + cache.putIfAbsent(LocalThumbProvider(id: 'asset-1', assetType: AssetType.image), load); + + expect(loads, 1); + }); + }); + + group('factories', () { + test('thumbnails are keyed by the asset checksum', () { + final asset = LocalAssetFactory.create().copyWith(checksum: 'abc'); + + final provider = getThumbnailImageProvider(asset)! as LocalThumbProvider; + + expect(provider.checksum, 'abc'); + }); + }); + + group('LocalFullImageProvider caching', () { + test('editing on device re-renders the full image', () { + cache.putIfAbsent( + LocalFullImageProvider( + id: 'asset-1', + assetType: AssetType.image, + size: const Size(100, 100), + isAnimated: false, + checksum: 'before', + ), + load, + ); + cache.putIfAbsent( + LocalFullImageProvider( + id: 'asset-1', + assetType: AssetType.image, + size: const Size(100, 100), + isAnimated: false, + checksum: 'after', + ), + load, + ); + + expect(loads, 2); + }); + }); +} diff --git a/mobile/test/presentation/widgets/timeline/timeline_args_test.dart b/mobile/test/presentation/widgets/timeline/timeline_args_test.dart index 0828e8e989..5c03998bfc 100644 --- a/mobile/test/presentation/widgets/timeline/timeline_args_test.dart +++ b/mobile/test/presentation/widgets/timeline/timeline_args_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: close_sinks + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/mobile/test/providers/app_life_cycle_provider_test.dart b/mobile/test/providers/app_life_cycle_provider_test.dart new file mode 100644 index 0000000000..d4489a58ba --- /dev/null +++ b/mobile/test/providers/app_life_cycle_provider_test.dart @@ -0,0 +1,180 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/models/auth/auth_state.model.dart'; +import 'package:immich_mobile/models/server_info/server_version.model.dart'; +import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/utils/upload_speed_calculator.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../infrastructure/repository.mock.dart'; +import '../service.mocks.dart'; + +class FakeLogMessage extends Fake implements LogMessage {} + +class TestAuthNotifier extends AuthNotifier { + TestAuthNotifier(Ref ref) + : super( + MockAuthService(), + MockApiService(), + MockUserService(), + MockSecureStorageService(), + MockWidgetService(), + ref, + ) { + state = const AuthState( + deviceId: 'device-1', + userId: 'user-1', + userEmail: 'user@example.com', + name: 'User', + profileImagePath: '', + isAdmin: false, + isAuthenticated: true, + ); + } + + @override + Future setOpenApiServiceEndpoint() async => 'http://test-server.com'; +} + +class TestWebsocketNotifier extends WebsocketNotifier { + TestWebsocketNotifier(super.ref); + + int connectCount = 0; + int disconnectCount = 0; + final connectCalled = Completer(); + + @override + void connect() { + connectCount++; + if (!connectCalled.isCompleted) { + connectCalled.complete(); + } + throw StateError('unexpected websocket connection'); + } + + @override + void disconnect() => disconnectCount++; +} + +class TestDriftBackupNotifier extends DriftBackupNotifier { + TestDriftBackupNotifier() : super(MockForegroundUploadService(), MockBackgroundUploadService(), UploadSpeedManager()); +} + +void main() { + late LogService logService; + late Completer serverVersion; + late MockServerInfoService serverInfoService; + late MockBackgroundWorkerLockService lockService; + late ProviderContainer container; + late TestWebsocketNotifier websocket; + late AppLifeCycleNotifier lifeCycle; + late int serverVersionCount; + + setUpAll(() async { + final logRepository = MockLogRepository(); + final settingsRepository = MockSettingsRepository(); + registerFallbackValue(FakeLogMessage()); + when(() => logRepository.truncate(limit: any(named: 'limit'))).thenAnswer((_) async {}); + when(() => logRepository.insert(any())).thenAnswer((_) async => true); + when(() => settingsRepository.appConfig).thenReturn(const AppConfig(logLevel: LogLevel.info)); + logService = await LogService.init( + logRepository: logRepository, + settingsRepository: settingsRepository, + shouldBuffer: false, + ); + }); + + tearDownAll(() => logService.dispose()); + + setUp(() { + serverVersion = Completer(); + serverInfoService = MockServerInfoService(); + lockService = MockBackgroundWorkerLockService(); + serverVersionCount = 0; + + when(() => serverInfoService.getServerVersion()).thenAnswer((_) { + serverVersionCount++; + return serverVersionCount == 1 ? serverVersion.future : Future.value(); + }); + when(() => lockService.lock()).thenAnswer((_) async {}); + when(() => lockService.unlock()).thenAnswer((_) async {}); + + container = ProviderContainer( + overrides: [ + authProvider.overrideWith(TestAuthNotifier.new), + serverInfoProvider.overrideWith((_) => ServerInfoNotifier(serverInfoService)), + websocketProvider.overrideWith((ref) { + return websocket = TestWebsocketNotifier(ref); + }), + driftBackupProvider.overrideWith((_) => TestDriftBackupNotifier()), + backgroundWorkerLockServiceProvider.overrideWithValue(lockService), + ], + ); + lifeCycle = container.read(appStateProvider.notifier); + }); + + tearDown(() => container.dispose()); + + Future startResume() async { + await lifeCycle.handleAppPause(); + unawaited(lifeCycle.handleAppResume()); + await untilCalled(() => serverInfoService.getServerVersion()); + } + + Future releaseResume() async { + serverVersion.complete(); + await Future.delayed(Duration.zero); + } + + test('pause during resume does not reconnect websocket', () async { + await startResume(); + await lifeCycle.handleAppPause(); + await releaseResume(); + + expect(lifeCycle.getAppState(), AppLifeCycleEnum.paused); + expect(serverVersionCount, 1); + expect(websocket.disconnectCount, 2); + expect(websocket.connectCount, 0); + }); + + test('inactive resume retries when the app resumes again', () async { + await startResume(); + lifeCycle.handleAppInactivity(); + await releaseResume(); + + unawaited(lifeCycle.handleAppResume()); + await websocket.connectCalled.future; + + expect(lifeCycle.getAppState(), AppLifeCycleEnum.resumed); + expect(serverVersionCount, 2); + expect(websocket.disconnectCount, 1); + expect(websocket.connectCount, 1); + }); + + test('pause after an inactive abort resumes once', () async { + await startResume(); + lifeCycle.handleAppInactivity(); + await releaseResume(); + await lifeCycle.handleAppPause(); + + unawaited(lifeCycle.handleAppResume()); + await websocket.connectCalled.future; + unawaited(lifeCycle.handleAppResume()); + await Future.delayed(Duration.zero); + + expect(lifeCycle.getAppState(), AppLifeCycleEnum.resumed); + expect(serverVersionCount, 2); + expect(websocket.disconnectCount, 2); + expect(websocket.connectCount, 1); + }); +} diff --git a/mobile/test/providers/backup/drift_backup_provider_test.dart b/mobile/test/providers/backup/drift_backup_provider_test.dart new file mode 100644 index 0000000000..205f564cd9 --- /dev/null +++ b/mobile/test/providers/backup/drift_backup_provider_test.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/services/background_upload.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/utils/upload_speed_calculator.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockForegroundUploadService extends Mock implements ForegroundUploadService {} + +class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} + +void main() { + late MockForegroundUploadService foregroundUploadService; + late MockBackgroundUploadService backgroundUploadService; + late DriftBackupNotifier notifier; + + setUpAll(() { + registerFallbackValue(Completer()); + registerFallbackValue(const UploadCallbacks()); + }); + + setUp(() { + foregroundUploadService = MockForegroundUploadService(); + backgroundUploadService = MockBackgroundUploadService(); + notifier = DriftBackupNotifier(foregroundUploadService, backgroundUploadService, UploadSpeedManager()); + addTearDown(() { + if (notifier.mounted) { + notifier.dispose(); + } + }); + }); + + void mockCounts({required int total, required int remainder, int processing = 0}) { + when( + () => foregroundUploadService.getBackupCounts('user-1'), + ).thenAnswer((_) async => (total: total, remainder: remainder, processing: processing)); + } + + // Drives a backup run so we can grab the onSuccess callback the notifier wires up. + Future startAndCaptureOnSuccess() async { + void Function(String, String)? onSuccess; + when(() => foregroundUploadService.uploadCandidates(any(), any(), callbacks: any(named: 'callbacks'))).thenAnswer(( + invocation, + ) async { + onSuccess = (invocation.namedArguments[#callbacks] as UploadCallbacks).onSuccess; + }); + await notifier.startForegroundBackup('user-1'); + return onSuccess!; + } + + group('foreground backup counts', () { + test('successes move one asset from remainder to backup', () async { + mockCounts(total: 25, remainder: 25); + final onSuccess = await startAndCaptureOnSuccess(); + + for (var i = 0; i < 10; i++) { + onSuccess('asset-$i', 'remote-$i'); + } + + expect(notifier.state.remainderCount, 15); + expect(notifier.state.backupCount, 10); + expect(notifier.state.backupCount + notifier.state.remainderCount, notifier.state.totalCount); + }); + + test('a duplicate success after pause and resume cannot go below zero', () async { + // #26215: app pauses mid-backup, sync has not recorded the upload yet, so the + // resumed run re-uploads the same asset and the server answers 200 duplicate. + // The start of each run re-baselines the counters from the DB, so the duplicate + // success is counted against a baseline that includes the asset again. + mockCounts(total: 1, remainder: 1); + + final firstRun = await startAndCaptureOnSuccess(); + expect(notifier.state.remainderCount, 1); + firstRun('asset-1', 'remote-1'); + expect(notifier.state.remainderCount, 0); + + notifier.stopForegroundBackup(reason: "test"); + + final resumedRun = await startAndCaptureOnSuccess(); + expect(notifier.state.remainderCount, 1); + verify(() => foregroundUploadService.getBackupCounts('user-1')).called(2); + + resumedRun('asset-1', 'remote-1'); + expect(notifier.state.remainderCount, 0); + expect(notifier.state.backupCount, 1); + }); + + test('a drifted counter state heals at run start', () async { + mockCounts(total: 91, remainder: 7); + notifier.state = notifier.state.copyWith(totalCount: 91, backupCount: 103, remainderCount: -12); + + await startAndCaptureOnSuccess(); + + expect(notifier.state.totalCount, 91); + expect(notifier.state.remainderCount, 7); + expect(notifier.state.backupCount, 84); + }); + + test('a late success after dispose does not throw', () async { + mockCounts(total: 2, remainder: 2); + final onSuccess = await startAndCaptureOnSuccess(); + notifier.dispose(); + + onSuccess('asset-1', 'remote-1'); + }); + }); +} diff --git a/mobile/test/providers/infrastructure/action_provider_test.dart b/mobile/test/providers/infrastructure/action_provider_test.dart deleted file mode 100644 index 7f5c8d4ec2..0000000000 --- a/mobile/test/providers/infrastructure/action_provider_test.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.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/user.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/action.service.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockActionService extends Mock implements ActionService {} - -class MockAssetService extends Mock implements AssetService {} - -class MockForegroundUploadService extends Mock implements ForegroundUploadService {} - -class MockUserService extends Mock implements UserService {} - -class FakeBuildContext extends Fake implements BuildContext {} - -final _user = UserDto(id: 'user-1', email: 'user@test.dev', name: 'user', profileChangedAt: DateTime(2026)); - -final _asset = RemoteAsset( - id: 'asset-1', - name: 'photo.jpg', - ownerId: 'user-1', - checksum: 'checksum-1', - type: AssetType.image, - createdAt: DateTime(2026, 6, 10, 10, 27), - updatedAt: DateTime(2026, 6, 10, 10, 27), - isEdited: false, -); - -void main() { - late ProviderContainer container; - late MockActionService actionService; - late MockAssetService assetService; - - setUpAll(() { - registerFallbackValue(FakeBuildContext()); - registerFallbackValue(_asset); - registerFallbackValue([]); - }); - - setUp(() { - actionService = MockActionService(); - assetService = MockAssetService(); - final userService = MockUserService(); - - when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => true); - when(() => assetService.watchAsset(any())).thenAnswer((_) => const Stream.empty()); - when(() => assetService.getExif(any())).thenAnswer((_) async => null); - when(() => userService.tryGetMyUser()).thenReturn(_user); - when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty()); - - container = ProviderContainer( - overrides: [ - actionServiceProvider.overrideWithValue(actionService), - assetServiceProvider.overrideWithValue(assetService), - foregroundUploadServiceProvider.overrideWithValue(MockForegroundUploadService()), - currentUserProvider.overrideWith((ref) => CurrentUserProvider(userService)), - ], - ); - addTearDown(container.dispose); - }); - - group('editDateTime', () { - test('refreshes the exif provider when editing from the viewer', () async { - container.read(assetViewerProvider.notifier).setAsset(_asset); - container.listen(assetExifProvider(_asset), (_, __) {}); - await container.read(assetExifProvider(_asset).future); - - final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.viewer, FakeBuildContext()); - - expect(result?.success, isTrue); - await container.read(assetExifProvider(_asset).future); - verify(() => assetService.getExif(_asset)).called(2); - }); - - test('leaves the exif provider cached when editing from the timeline', () async { - container.read(assetViewerProvider.notifier).setAsset(_asset); - container.listen(assetExifProvider(_asset), (_, __) {}); - await container.read(assetExifProvider(_asset).future); - - final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.timeline, FakeBuildContext()); - - expect(result?.success, isTrue); - await container.read(assetExifProvider(_asset).future); - verify(() => assetService.getExif(_asset)).called(1); - }); - - test('does not refresh the exif provider when the edit is cancelled', () async { - when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => false); - container.read(assetViewerProvider.notifier).setAsset(_asset); - container.listen(assetExifProvider(_asset), (_, __) {}); - await container.read(assetExifProvider(_asset).future); - - final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.viewer, FakeBuildContext()); - - expect(result, isNull); - await container.read(assetExifProvider(_asset).future); - verify(() => assetService.getExif(_asset)).called(1); - }); - }); -} diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 82c9395b58..b56a8a098a 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -6,7 +6,6 @@ import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; -import 'package:immich_mobile/repositories/toast.repository.dart'; import 'package:mocktail/mocktail.dart'; class MockAssetApiRepository extends Mock implements AssetApiRepository {} @@ -24,5 +23,3 @@ class MockTagService extends Mock implements TagService {} class MockDownloadRepository extends Mock implements DownloadRepository {} class MockRemoteExifRepository extends Mock implements RemoteExifRepository {} - -class MockToastRepository extends Mock implements ToastRepository {} diff --git a/mobile/test/riverpod_mocks.dart b/mobile/test/riverpod_mocks.dart new file mode 100644 index 0000000000..24867b226e --- /dev/null +++ b/mobile/test/riverpod_mocks.dart @@ -0,0 +1,8 @@ +import 'package:immich_mobile/models/server_info/server_version.model.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; + +class StubServerInfoNotifier extends ServerInfoNotifier { + StubServerInfoNotifier(super.serverInfoService, {required ServerVersion version}) { + state = state.copyWith(serverVersion: version); + } +} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 300c54dcbb..ecf9c771bc 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,4 +1,5 @@ import 'package:immich_mobile/domain/services/asset.service.dart'; +import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; @@ -7,11 +8,16 @@ import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/auth.service.dart'; +import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:immich_mobile/services/cleanup.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/services/network.service.dart'; +import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; +import 'package:immich_mobile/services/toast.service.dart'; +import 'package:immich_mobile/services/widget.service.dart'; import 'package:mocktail/mocktail.dart'; class MockApiService extends Mock implements ApiService {} @@ -43,3 +49,15 @@ class MockServerInfoService extends Mock implements ServerInfoService {} class MockCleanupService extends Mock implements CleanupService {} class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} + +class MockToastService extends Mock implements ToastService {} + +class MockAuthService extends Mock implements AuthService {} + +class MockSecureStorageService extends Mock implements SecureStorageService {} + +class MockWidgetService extends Mock implements WidgetService {} + +class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} + +class MockBackgroundWorkerLockService extends Mock implements BackgroundWorkerLockService {} diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index 72691a3802..2d90e24503 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -2,7 +2,6 @@ import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -18,13 +17,6 @@ void main() { late MockAssetApiRepository assetApiRepository; late MockRemoteAssetRepository remoteAssetRepository; - late MockDriftLocalAssetRepository localAssetRepository; - late MockDriftAlbumApiRepository albumApiRepository; - late MockRemoteAlbumRepository remoteAlbumRepository; - late MockTrashedLocalAssetRepository trashedLocalAssetRepository; - late MockAssetMediaRepository assetMediaRepository; - late MockDownloadRepository downloadRepository; - late MockTagService tagService; late Drift db; @@ -45,25 +37,8 @@ void main() { setUp(() { assetApiRepository = MockAssetApiRepository(); remoteAssetRepository = MockRemoteAssetRepository(); - localAssetRepository = MockDriftLocalAssetRepository(); - albumApiRepository = MockDriftAlbumApiRepository(); - remoteAlbumRepository = MockRemoteAlbumRepository(); - trashedLocalAssetRepository = MockTrashedLocalAssetRepository(); - assetMediaRepository = MockAssetMediaRepository(); - downloadRepository = MockDownloadRepository(); - tagService = MockTagService(); - sut = ActionService( - assetApiRepository, - remoteAssetRepository, - localAssetRepository, - albumApiRepository, - remoteAlbumRepository, - trashedLocalAssetRepository, - assetMediaRepository, - downloadRepository, - tagService, - ); + sut = ActionService(assetApiRepository, remoteAssetRepository); }); tearDown(() async { @@ -95,93 +70,4 @@ void main() { verify(() => remoteAssetRepository.updateRating(assetId, null)).called(1); }); }); - - group('ActionService.applyDateTime', () { - const ids = ['asset_id_1']; - - test('sends the picked value to the api with its offset intact', () async { - const picked = '2026-06-10T19:15:00.000+06:00'; - when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {}); - when( - () => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC+06:00'), - ).thenAnswer((_) async {}); - - await sut.applyDateTime(ids, picked); - - verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1); - verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC+06:00')).called(1); - }); - - test('handles negative offsets', () async { - const picked = '2026-01-05T08:00:00.000-05:30'; - when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {}); - when( - () => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC-05:30'), - ).thenAnswer((_) async {}); - - await sut.applyDateTime(ids, picked); - - verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1); - verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC-05:30')).called(1); - }); - - test('writes no timezone when the value has no offset', () async { - const picked = '2026-06-10T13:15:00.000Z'; - when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {}); - when( - () => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: null), - ).thenAnswer((_) async {}); - - await sut.applyDateTime(ids, picked); - - verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1); - verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: null)).called(1); - }); - }); - - group('ActionService.deleteLocal', () { - test('routes deleted ids to trashed repository when Android trash handling is enabled', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, true); - const ids = ['a', 'b']; - - when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); - when(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).thenAnswer((_) async {}); - - final result = await sut.deleteLocal(ids); - - expect(result, ids.length); - verify(() => assetMediaRepository.deleteAll(ids)).called(1); - verify(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).called(1); - verifyNever(() => localAssetRepository.delete(any())); - }); - - test('deletes locally when Android trash handling is disabled', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, false); - const ids = ['c']; - - when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); - when(() => localAssetRepository.delete(ids)).thenAnswer((_) async {}); - - final result = await sut.deleteLocal(ids); - - expect(result, ids.length); - verify(() => assetMediaRepository.deleteAll(ids)).called(1); - verify(() => localAssetRepository.delete(ids)).called(1); - verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any())); - }); - - test('short-circuits when nothing was deleted', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, true); - const ids = ['x']; - - when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => []); - - final result = await sut.deleteLocal(ids); - - expect(result, 0); - verify(() => assetMediaRepository.deleteAll(ids)).called(1); - verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any())); - verifyNever(() => localAssetRepository.delete(any())); - }); - }); } diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index d71a52f2ae..b62ad3ad2c 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -103,7 +103,7 @@ void main() { }); test('Should return null if auto endpoint switching is disabled', () async { - when(() => authRepository.getEndpointSwitchingFeature()).thenReturn((false)); + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(false); final result = await sut.setOpenApiServiceEndpoint(); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index 65f17501a0..aa46bd9de3 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/mobile/test/services/deep_link_service_test.dart b/mobile/test/services/deep_link_service_test.dart index ff090367ea..16fa392f27 100644 --- a/mobile/test/services/deep_link_service_test.dart +++ b/mobile/test/services/deep_link_service_test.dart @@ -115,7 +115,7 @@ void main() { final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref); expect(route, isA()); - expect((route!.args as AssetViewerRouteArgs).currentAlbum, _album); + expect((route!.args! as AssetViewerRouteArgs).currentAlbum, _album); }); test('still opens the viewer when the album cannot be resolved', () async { @@ -125,7 +125,7 @@ void main() { final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref); expect(route, isA()); - expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull); + expect((route!.args! as AssetViewerRouteArgs).currentAlbum, isNull); }); test('plain photo link has no album', () async { @@ -134,7 +134,7 @@ void main() { final route = await sut.handleMyImmichApp(link('/photos/$_assetId'), ref); expect(route, isA()); - expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull); + expect((route!.args! as AssetViewerRouteArgs).currentAlbum, isNull); verifyNever(() => remoteAlbumService.get(any())); }); } diff --git a/mobile/test/services/foreground_upload.service_test.dart b/mobile/test/services/foreground_upload.service_test.dart index 46e9a82141..d4344ed77e 100644 --- a/mobile/test/services/foreground_upload.service_test.dart +++ b/mobile/test/services/foreground_upload.service_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/mobile/test/services/view_intent_service_test.dart b/mobile/test/services/view_intent_service_test.dart index 7b3d0b85e7..fd8f5f725c 100644 --- a/mobile/test/services/view_intent_service_test.dart +++ b/mobile/test/services/view_intent_service_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_slow_async_io + import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; @@ -13,11 +15,7 @@ void main() { late Directory tempRoot; late Directory cacheDir; - final attachment = ViewIntentPayload( - path: '/tmp/file.jpg', - mimeType: 'image/jpeg', - localAssetId: '42', - ); + final attachment = ViewIntentPayload(path: '/tmp/file.jpg', mimeType: 'image/jpeg', localAssetId: '42'); setUp(() { hostApi = MockViewIntentHostApi(); diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index d8eadda7ae..1d43198263 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -30,7 +30,6 @@ class RepositoryMocks { final remoteAsset = RemoteAssetRepositoryStub(MockRemoteAssetRepository()); final remoteExif = RemoteExifRepositoryStub(MockRemoteExifRepository()); final trashedAsset = MockTrashedLocalAssetRepository(); - final toast = MockToastRepository(); final remoteAlbum = MockRemoteAlbumRepository(); final albumApi = MockDriftAlbumApiRepository(); @@ -56,7 +55,6 @@ class RepositoryMocks { assetApi.reset(); assetMedia.reset(); download.reset(); - reset(toast); _stubLocalAlbumRepository(); _stubLocalAssetRepository(); _stubRemoteAssetRepository(); @@ -97,6 +95,7 @@ class RepositoryMocks { void _stubAssetMediaRepository() { when(assetMedia.shareAssets).thenAnswer((_) async => 1); + when(assetMedia.getOriginalFilename).thenAnswer((_) async => null); } void _stubDownloadRepository() { @@ -115,6 +114,7 @@ class ServiceMocks { final upload = MockForegroundUploadService(); final cast = MockGCastService(); final serverInfo = MockServerInfoService(); + final toast = MockToastService(); ServiceMocks() { resetAll(); @@ -132,6 +132,7 @@ class ServiceMocks { reset(serverInfo); reset(backgroundSync); reset(upload); + reset(toast); _stubUserService(); _stubPartnerService(); _stubAssetService(); @@ -167,6 +168,7 @@ class ServiceMocks { when(asset.trash).thenAnswer((_) async {}); when(asset.delete).thenAnswer((_) async {}); when(asset.applyEdits).thenAnswer((_) async {}); + when(asset.deleteLocal).thenAnswer((_) async => 0); } void _stubRemoteAlbumService() { @@ -342,6 +344,9 @@ extension type const AssetServiceStub(MockAssetService service) implements Stub< Future Function() get applyEdits => () => service.applyEdits(any(), any()); + + Future Function() get deleteLocal => + () => service.deleteLocal(any()); } extension type const RemoteAlbumServiceStub(MockRemoteAlbumService service) implements Stub { @@ -385,6 +390,9 @@ extension type const AssetMediaRepositoryStub(MockAssetMediaRepository api) impl cancelCompleter: any(named: 'cancelCompleter'), onAssetDownloadProgress: any(named: 'onAssetDownloadProgress'), ); + + Future Function() get getOriginalFilename => + () => api.getOriginalFilename(any()); } extension type const DownloadRepositoryStub(MockDownloadRepository repo) implements Stub { diff --git a/mobile/test/unit/presentation/action_buttons/share_action_button_test.dart b/mobile/test/unit/presentation/action_buttons/share_action_button_test.dart deleted file mode 100644 index 2f4aa3b8c9..0000000000 --- a/mobile/test/unit/presentation/action_buttons/share_action_button_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/settings_key.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; - -import '../../factories/remote_asset_factory.dart'; -import '../presentation_context.dart'; - -class _RecordingActionNotifier extends ActionNotifier { - final List sharedFileTypes = []; - - @override - void build() {} - - @override - Future shareAssets( - ActionSource source, - BuildContext context, { - ShareAssetType fileType = ShareAssetType.original, - Completer? cancelCompleter, - void Function(double progress)? onAssetDownloadProgress, - }) async { - sharedFileTypes.add(fileType); - return const ActionResult(count: 1, success: true); - } -} - -class _FakeAssetViewerNotifier extends AssetViewerStateNotifier { - final BaseAsset asset; - - _FakeAssetViewerNotifier(this.asset); - - @override - AssetViewerState build() => AssetViewerState(currentAsset: asset); -} - -void main() { - late PresentationContext context; - late _RecordingActionNotifier actionNotifier; - - setUpAll(() async { - final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await SettingsRepository.ensureInitialized(db); - }); - - setUp(() async { - context = await PresentationContext.create(); - actionNotifier = _RecordingActionNotifier(); - await SettingsRepository.instance.clear([SettingsKey.shareFileType]); - }); - - tearDown(() { - context.dispose(); - }); - - Future pumpShareButton(WidgetTester tester) async { - final asset = RemoteAssetFactory.create(ownerId: context.currentUser.id); - await tester.pumpTestWidget( - context, - const ShareActionButton(source: ActionSource.viewer), - overrides: [ - actionProvider.overrideWith(() => actionNotifier), - assetViewerProvider.overrideWith(() => _FakeAssetViewerNotifier(asset)), - ], - ); - } - - Future longPressAndPickPreview(WidgetTester tester) async { - await tester.longPress(find.byType(BaseActionButton)); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.photo_size_select_large_rounded)); - await tester.pumpAndSettle(); - } - - group('ShareActionButton', () { - testWidgets('single press shares with the configured default quality', (tester) async { - await pumpShareButton(tester); - - await tester.tap(find.byType(BaseActionButton)); - await tester.pumpAndSettle(); - - expect(actionNotifier.sharedFileTypes, [ShareAssetType.original]); - }); - - testWidgets('long press shares with the quality picked in the dialog', (tester) async { - await pumpShareButton(tester); - - await longPressAndPickPreview(tester); - - expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]); - }); - - testWidgets('quality picked on long press is a one-time choice and does not change the default', (tester) async { - await pumpShareButton(tester); - - await longPressAndPickPreview(tester); - expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]); - - await tester.tap(find.byType(BaseActionButton)); - await tester.pumpAndSettle(); - - expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview, ShareAssetType.original]); - expect(SettingsRepository.instance.appConfig.share.fileType, ShareAssetType.original); - }); - }); -} diff --git a/mobile/test/unit/presentation/actions/album_action_test.dart b/mobile/test/unit/presentation/actions/album_action_test.dart new file mode 100644 index 0000000000..7baa14a4eb --- /dev/null +++ b/mobile/test/unit/presentation/actions/album_action_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.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/remove_from_album.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_album_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + const albumId = 'album-1'; + + late PresentationContext context; + late MockRemoteAlbumService albumService; + + setUp(() async { + context = await PresentationContext.create(); + albumService = context.service.album.service; + }); + + tearDown(() async { + await context.dispose(); + }); + + List withMockToast() => [toastServiceProvider.overrideWithValue(context.service.toast)]; + + group('RemoveFromAlbumAction', () { + Future pumpRemove(WidgetTester tester, Set selection) => tester.pumpTestAction( + context, + const RemoveFromAlbumAction(source: .timeline, albumId: albumId), + overrides: [...context.selected(selection), ...withMockToast()], + ); + + testWidgets('removes every selected remote asset from the album', (tester) async { + final first = RemoteAssetFactory.create(); + final second = RemoteAssetFactory.create(); + + await pumpRemove(tester, {first, second}); + await tester.pumpAndSettle(); + + verify(() => albumService.removeAssets(albumId: albumId, assetIds: [first.id, second.id])).called(1); + }); + + testWidgets('reports the count the server actually removed', (tester) async { + when( + () => albumService.removeAssets( + albumId: any(named: 'albumId'), + assetIds: any(named: 'assetIds'), + ), + ).thenAnswer((_) async => 1); + + await pumpRemove(tester, {RemoteAssetFactory.create(), RemoteAssetFactory.create()}); + await tester.pumpAndSettle(); + + final message = verify(() => context.service.toast.success(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.remove_from_album_action_prompt(count: 1)); + }); + + testWidgets('is hidden for a local-only asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: RemoveFromAlbumAction(source: .timeline, albumId: albumId), + ), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('SetAlbumCoverAction', () { + testWidgets('sets the single selected asset as the cover', (tester) async { + final asset = RemoteAssetFactory.create(); + when( + () => albumService.updateAlbum(any(), thumbnailAssetId: any(named: 'thumbnailAssetId')), + ).thenAnswer((_) async => RemoteAlbumFactory.create(id: albumId)); + + await tester.pumpTestAction( + context, + const SetAlbumCoverAction(source: .timeline, albumId: albumId), + overrides: [ + ...context.selected({asset}), + ...withMockToast(), + ], + ); + await tester.pumpAndSettle(); + + verify(() => albumService.updateAlbum(albumId, thumbnailAssetId: asset.id)).called(1); + + final message = verify(() => context.service.toast.success(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.album_cover_updated); + }); + + testWidgets('is hidden for more than one asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: SetAlbumCoverAction(source: .timeline, albumId: albumId), + ), + overrides: context.selected({RemoteAssetFactory.create(), RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for a local-only asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: SetAlbumCoverAction(source: .timeline, albumId: albumId), + ), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/archive_action_test.dart b/mobile/test/unit/presentation/actions/archive_action_test.dart new file mode 100644 index 0000000000..555d6af2ff --- /dev/null +++ b/mobile/test/unit/presentation/actions/archive_action_test.dart @@ -0,0 +1,118 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() async { + await context.dispose(); + }); + + RemoteAsset owned({AssetVisibility visibility = .timeline}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility); + + Future pumpArchive(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const ArchiveAction(source: .timeline), overrides: context.selected(selection)); + + group('ArchiveAction', () { + testWidgets('archives the eligible owned assets', (tester) async { + final asset = owned(); + + await pumpArchive(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const Option.some(AssetVisibility.archive))).called(1); + }); + + testWidgets('unarchive the eligible owned assets', (tester) async { + final asset = owned(visibility: .archive); + + await pumpArchive(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const .some(.timeline))).called(1); + }); + + testWidgets('prioritizes archive when mixed state', (tester) async { + final onTimeline = owned(); + final archived = owned(visibility: .archive); + + await pumpArchive(tester, {onTimeline, archived}); + + verify(() => assetService.update([onTimeline.id], visibility: const .some(.archive))).called(1); + verifyNever(() => assetService.update(any(), visibility: const .some(.timeline))); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpArchive(tester, {mine, theirs}); + + verify(() => assetService.update([mine.id], visibility: const .some(.archive))).called(1); + }); + + testWidgets('skips owned assets already in the target state', (tester) async { + final stale = owned(); + final alreadyArchived = owned(visibility: .archive); + + await pumpArchive(tester, {stale, alreadyArchived}); + + verify(() => assetService.update([stale.id], visibility: const .some(.archive))).called(1); + }); + + testWidgets('clears the selection once the update succeeds', (tester) async { + await pumpArchive(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden for locked assets, which belong to neither direction', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ArchiveAction(source: .timeline)), + overrides: context.selected({owned(visibility: .locked)}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden inside the locked folder view', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ArchiveAction(source: .timeline)), + overrides: [ + ...context.selected({owned()}), + inLockedViewProvider.overrideWithValue(true), + ], + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ArchiveAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/asset_debug_action_test.dart b/mobile/test/unit/presentation/actions/asset_debug_action_test.dart index 4e84c100cb..f545075809 100644 --- a/mobile/test/unit/presentation/actions/asset_debug_action_test.dart +++ b/mobile/test/unit/presentation/actions/asset_debug_action_test.dart @@ -16,15 +16,16 @@ void main() { await StoreService.I.put(StoreKey.advancedTroubleshooting, true); }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); group('AssetDebugAction', () { testWidgets('visible for a single asset when advanced troubleshooting is on', (tester) async { await tester.pumpTestWidget( context, - ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])), + const ActionIconButton(action: AssetDebugAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), ); expect(find.byType(ImmichIconButton), findsOneWidget); @@ -33,9 +34,8 @@ void main() { testWidgets('hidden for multiple assets', (tester) async { await tester.pumpTestWidget( context, - ActionIconButtonWidget( - action: AssetDebugAction(assets: [RemoteAssetFactory.create(), RemoteAssetFactory.create()]), - ), + const ActionIconButton(action: AssetDebugAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create(), RemoteAssetFactory.create()}), ); expect(find.byType(ImmichIconButton), findsNothing); @@ -43,9 +43,11 @@ void main() { testWidgets('hidden when advanced troubleshooting is off', (tester) async { await StoreService.I.put(StoreKey.advancedTroubleshooting, false); + await tester.pumpTestWidget( context, - ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])), + const ActionIconButton(action: AssetDebugAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), ); expect(find.byType(ImmichIconButton), findsNothing); diff --git a/mobile/test/unit/presentation/actions/cast_action_test.dart b/mobile/test/unit/presentation/actions/cast_action_test.dart new file mode 100644 index 0000000000..7c16ea398e --- /dev/null +++ b/mobile/test/unit/presentation/actions/cast_action_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/cast.action.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUp(() async { + context = await PresentationContext.create(); + }); + + tearDown(() async { + await context.dispose(); + }); + + void Function(bool) captureConnectionListener() => + verify(() => context.service.cast.onConnectionState = captureAny()).captured.single as void Function(bool); + + group('CastAction', () { + testWidgets('offers to cast when nothing is connected', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + expect(find.byIcon(Icons.cast_rounded), findsOneWidget); + }); + + testWidgets('switches to the connected icon once casting starts', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + captureConnectionListener()(true); + await tester.pump(); + + expect(find.byIcon(Icons.cast_connected_rounded), findsOneWidget); + expect(find.byIcon(Icons.cast_rounded), findsNothing); + }); + + testWidgets('switches back when casting stops', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + final onConnectionState = captureConnectionListener(); + onConnectionState(true); + await tester.pump(); + onConnectionState(false); + await tester.pump(); + + expect(find.byIcon(Icons.cast_rounded), findsOneWidget); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/delete_action_test.dart b/mobile/test/unit/presentation/actions/delete_action_test.dart new file mode 100644 index 0000000000..29d81a5c2c --- /dev/null +++ b/mobile/test/unit/presentation/actions/delete_action_test.dart @@ -0,0 +1,257 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.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/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + late MockCleanupService cleanupService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + cleanupService = context.service.cleanup.service; + }); + + tearDown(() async { + debugDefaultTargetPlatformOverride = null; + await StoreService.I.put(StoreKey.manageLocalMediaAndroid, false); + await context.dispose(); + }); + + RemoteAsset owned({AssetVisibility visibility = .timeline, DateTime? deletedAt, String? localId}) => + RemoteAssetFactory.create( + ownerId: context.currentUser.id, + visibility: visibility, + deletedAt: deletedAt, + localId: localId, + ); + + Future pumpDelete(WidgetTester tester, Set selection, {bool trashEnabled = true}) async { + if (!trashEnabled) { + when( + () => context.service.serverInfo.getServerFeatures(), + ).thenAnswer((_) async => const .new(trash: false, map: true, oauthEnabled: false, passwordLogin: true)); + } + + await tester.pumpTestWidget( + context, + const ActionIconButton(action: DeleteAction(source: .timeline)), + overrides: context.selected(selection), + ); + + if (!trashEnabled) { + final scope = ProviderScope.containerOf(tester.element(find.byType(ActionIconButton)), listen: false); + await scope.read(serverInfoProvider.notifier).getServerFeatures(); + await tester.pumpAndSettle(); + } + + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + } + + Future respondToDialog(WidgetTester tester, {required bool confirm}) async { + await tester.pump(const Duration(milliseconds: 300)); + expect(find.byType(ConfirmDialog), findsOneWidget); + await tester.tap(find.byType(TextButton).at(confirm ? 1 : 0)); // [cancel, ok] + await tester.pumpAndSettle(); + } + + group('DeleteAction', () { + group('trash', () { + testWidgets('trashes a remote-only owned asset', (tester) async { + final asset = owned(); + + await pumpDelete(tester, {asset}); + await tester.pumpAndSettle(); + + verify(() => assetService.trash([asset.id])).called(1); + verifyNever(() => assetService.delete(any())); + verifyNever(() => cleanupService.deleteLocalAssets(any())); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpDelete(tester, {mine, theirs}); + await tester.pumpAndSettle(); + + verify(() => assetService.trash([mine.id])).called(1); + }); + + testWidgets('trashes a merged asset and removes its device copy', (tester) async { + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}); + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + verify(() => assetService.trash([asset.id])).called(1); + }); + }); + + group('permanent', () { + testWidgets('permanently deletes when the trash feature is disabled', (tester) async { + final asset = owned(); + + await pumpDelete(tester, {asset}, trashEnabled: false); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verifyNever(() => assetService.trash(any())); + }); + + testWidgets('permanently deletes a merged asset and removes its device copy', (tester) async { + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}, trashEnabled: false); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + }); + + testWidgets('permanently deletes already trashed assets even with trash enabled', (tester) async { + final asset = owned(deletedAt: DateTime(2024)); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verifyNever(() => assetService.trash(any())); + }); + + testWidgets('permanently deletes locked folder assets even with trash enabled', (tester) async { + final asset = owned(visibility: .locked, localId: 'local'); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + }); + + testWidgets('does nothing when the confirmation is cancelled', (tester) async { + final asset = owned(visibility: .locked, localId: 'local'); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: false); + + verifyNever(() => assetService.delete(any())); + verifyNever(() => cleanupService.deleteLocalAssets(any())); + }); + }); + + group('local only', () { + testWidgets('removes the device copy with no remote call', (tester) async { + final asset = LocalAssetFactory.create(); + + await pumpDelete(tester, {asset}); + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets([asset.id])).called(1); + verifyNever(() => assetService.trash(any())); + verifyNever(() => assetService.delete(any())); + }); + }); + + group('prompt handling', () { + testWidgets('permanent delete shows a single app dialog', (tester) async { + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}, trashEnabled: false); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.text(StaticTranslations.instance.delete_dialog_title), findsOneWidget); + await tester.tap(find.byType(TextButton).at(1)); + await tester.pumpAndSettle(); + + expect(find.text(StaticTranslations.instance.move_to_device_trash), findsNothing); + verify(() => assetService.delete([asset.id])).called(1); + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + }); + + testWidgets('local only delete on Android with MANAGE_MEDIA shows the prompt', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true); + final asset = LocalAssetFactory.create(); + + await pumpDelete(tester, {asset}); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.text(StaticTranslations.instance.move_to_device_trash), findsOneWidget); + await tester.tap(find.byType(TextButton).at(1)); // confirm + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets([asset.id])).called(1); + // Has to be cleared inside the body; the framework asserts on it before tearDown runs. + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('local only delete on Android with MANAGE_MEDIA deletes nothing when cancelled', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true); + final asset = LocalAssetFactory.create(); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: false); + + verifyNever(() => cleanupService.deleteLocalAssets(any())); + debugDefaultTargetPlatformOverride = null; + }); + }); + + testWidgets('is hidden when nothing can be deleted', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: DeleteAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('CleanupLocalAction', () { + testWidgets('deletes only backed up device copies', (tester) async { + final backedUp = LocalAssetFactory.create(remoteId: 'remote'); + final localOnly = LocalAssetFactory.create(); + + await tester.pumpTestAction( + context, + const CleanupLocalAction(source: .timeline), + overrides: context.selected({backedUp, localOnly}), + ); + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets([backedUp.id])).called(1); + }); + + testWidgets('is hidden when no backed up assets are selected', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: CleanupLocalAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/download_tag_action_test.dart b/mobile/test/unit/presentation/actions/download_tag_action_test.dart new file mode 100644 index 0000000000..de003ffed5 --- /dev/null +++ b/mobile/test/unit/presentation/actions/download_tag_action_test.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.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/tag.model.dart'; +import 'package:immich_mobile/domain/services/tag.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/tag.action.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../repository.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockTagService tagService; + + setUp(() async { + context = await PresentationContext.create(); + tagService = context.service.tag.service; + }); + + tearDown(() async { + await context.dispose(); + }); + + RemoteAsset owned() => RemoteAssetFactory.create(ownerId: context.currentUser.id); + + group('DownloadAction', () { + Future pumpDownload(WidgetTester tester, Set selection) => tester.pumpTestAction( + context, + const DownloadAction(source: .timeline), + overrides: [ + ...context.selected(selection), + downloadRepositoryProvider.overrideWithValue(context.repository.download.repo), + backgroundSyncProvider.overrideWithValue(context.service.backgroundSync), + ], + ); + + Future settleDownload(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const .new(seconds: 1)); + await tester.pumpAndSettle(); + } + + testWidgets('downloads every selected remote asset', (tester) async { + final asset = owned(); + + await pumpDownload(tester, {asset}); + await settleDownload(tester); + + verify(() => context.repository.download.repo.downloadAllAssets([asset])).called(1); + }); + + testWidgets('ignores local-only assets, which are already on the device', (tester) async { + final remote = owned(); + + await pumpDownload(tester, {remote, LocalAssetFactory.create()}); + await settleDownload(tester); + + verify(() => context.repository.download.repo.downloadAllAssets([remote])).called(1); + }); + + testWidgets('is hidden when nothing remote is selected', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: DownloadAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('TagAction', () { + late BuildContext actionContext; + late WidgetRef actionRef; + + Future pumpTag(WidgetTester tester, Set selection) => tester.pumpTestWidget( + context, + Consumer( + builder: (widgetContext, ref, _) { + actionContext = widgetContext; + actionRef = ref; + return const ActionIconButton(action: TagAction(source: .timeline)); + }, + ), + overrides: [ + ...context.selected(selection), + toastServiceProvider.overrideWithValue(context.service.toast), + tagServiceProvider.overrideWithValue(tagService), + userMetadataPreferencesProvider.overrideWith((ref) async => const .new(tagsEnabled: true)), + ], + ); + + Future applyTags(List assetIds, {Set selected = const {}, Set created = const {}}) => + tagAssets(actionContext, actionRef, assetIds, selected: selected, created: created); + + testWidgets('offers tagging for an owned asset', (tester) async { + await pumpTag(tester, {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden without any owned asset', (tester) async { + await pumpTag(tester, {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('applies the picked tags and reports the count', (tester) async { + final asset = owned(); + when(() => tagService.bulkTagAssets(any(), any())).thenAnswer((_) async => 1); + + await pumpTag(tester, {asset}); + await applyTags([asset.id], selected: {'tag-1'}); + await tester.pumpAndSettle(); + + verify(() => tagService.bulkTagAssets([asset.id], ['tag-1'])).called(1); + + final message = verify(() => context.service.toast.success(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.tagged_assets(count: 1)); + }); + + testWidgets('creates new tags first and applies them alongside the picked ones', (tester) async { + final asset = owned(); + when(() => tagService.upsertTags(any())).thenAnswer((_) async => [const Tag(id: 'made-1', value: 'brand new')]); + when(() => tagService.bulkTagAssets(any(), any())).thenAnswer((_) async => 1); + + await pumpTag(tester, {asset}); + await applyTags([asset.id], selected: {'tag-1'}, created: {'brand new'}); + await tester.pumpAndSettle(); + + verify(() => tagService.upsertTags(['brand new'])).called(1); + final tagIds = verify(() => tagService.bulkTagAssets([asset.id], captureAny())).captured.single as List; + expect(tagIds, containsAll(['tag-1', 'made-1'])); + }); + + testWidgets('does nothing when no tag was chosen or created', (tester) async { + final asset = owned(); + + await pumpTag(tester, {asset}); + await applyTags([asset.id]); + await tester.pumpAndSettle(); + + verifyNever(() => tagService.bulkTagAssets(any(), any())); + verifyNever(() => context.service.toast.success(any())); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/edit_action_test.dart b/mobile/test/unit/presentation/actions/edit_action_test.dart new file mode 100644 index 0000000000..9e8dcfb4b2 --- /dev/null +++ b/mobile/test/unit/presentation/actions/edit_action_test.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/models/server_info/server_version.model.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/edit_asset.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../riverpod_mocks.dart'; +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() async { + await context.dispose(); + }); + + RemoteAsset owned({AssetType type = .image}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, type: type); + + const supportedVersion = ServerVersion(major: 2, minor: 6, patch: 0); + + List reportedVersion(ServerVersion version) => [ + serverInfoProvider.overrideWith((ref) => StubServerInfoNotifier(context.service.serverInfo, version: version)), + ]; + + late BuildContext actionContext; + late WidgetRef actionRef; + + Future pumpAction( + WidgetTester tester, + ActionBuilder action, + Set selection, { + List overrides = const [], + }) => tester.pumpTestWidget( + context, + Consumer( + builder: (widgetContext, ref, _) { + actionContext = widgetContext; + actionRef = ref; + return ActionIconButton(action: action); + }, + ), + overrides: [...context.selected(selection), ...overrides], + ); + + group('EditAssetAction', () { + Future pumpEditAsset( + WidgetTester tester, + Set selection, { + ServerVersion version = supportedVersion, + }) => pumpAction( + tester, + const EditAssetAction(source: .timeline), + selection, + overrides: [...reportedVersion(version)], + ); + + testWidgets('offers to edit a single owned editable asset', (tester) async { + await pumpEditAsset(tester, {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden when the server predates edit sync', (tester) async { + await pumpEditAsset(tester, {owned()}, version: const ServerVersion(major: 2, minor: 5, patch: 9)); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for more than one asset', (tester) async { + await pumpEditAsset(tester, {owned(), owned()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for an asset owned by someone else', (tester) async { + await pumpEditAsset(tester, {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for a non-editable asset', (tester) async { + await pumpEditAsset(tester, {owned(type: .video)}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('reads the edits and exif for the asset it is about to open', (tester) async { + final asset = owned(); + + await pumpEditAsset(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + + verify(() => context.repository.remoteAsset.repo.getAssetEdits(asset.id)).called(1); + verify(() => context.repository.remoteAsset.repo.getExif(asset.id)).called(1); + }); + }); + + group('EditLocationAction', () { + testWidgets('offers to edit an owned remote asset', (tester) async { + await pumpAction(tester, const EditLocationAction(source: .timeline), {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden without any owned remote asset', (tester) async { + await pumpAction(tester, const EditLocationAction(source: .timeline), {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('saves the location against every owned asset and toasts the count', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpAction(tester, const EditLocationAction(source: .timeline), {mine, theirs}); + await saveLocation(actionContext, actionRef, [mine.id], const LatLng(1, 2)); + await tester.pumpAndSettle(); + + final location = + verify(() => assetService.update([mine.id], location: captureAny(named: 'location'))).captured.single + as Option; + expect(location.unwrapOrNull?.latitude, 1); + expect(location.unwrapOrNull?.longitude, 2); + }); + }); + + group('EditDateTimeAction', () { + testWidgets('offers to edit an owned remote asset', (tester) async { + await pumpAction(tester, const EditDateTimeAction(source: .timeline), {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden without any owned remote asset', (tester) async { + await pumpAction(tester, const EditDateTimeAction(source: .timeline), {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('saves the date against every owned asset and toasts the count', (tester) async { + const picked = '2026-06-10T19:15:00.000+06:00'; + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpAction(tester, const EditDateTimeAction(source: .timeline), {mine, theirs}); + await saveDateTime(actionContext, actionRef, [mine.id], picked); + await tester.pumpAndSettle(); + + verify(() => assetService.update([mine.id], dateTime: const Some(picked))).called(1); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/favorite_action_test.dart b/mobile/test/unit/presentation/actions/favorite_action_test.dart index 722d9d1dc7..4d61965b1f 100644 --- a/mobile/test/unit/presentation/actions/favorite_action_test.dart +++ b/mobile/test/unit/presentation/actions/favorite_action_test.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:immich_ui/immich_ui.dart'; import 'package:mocktail/mocktail.dart'; import '../../../service.mocks.dart'; @@ -17,62 +20,73 @@ void main() { assetService = context.service.asset.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned({bool isFavorite = false}) => RemoteAssetFactory.create(ownerId: context.currentUser.id, isFavorite: isFavorite); + Future pumpFavorite(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const FavoriteAction(source: .timeline), overrides: context.selected(selection)); + group('FavoriteAction', () { testWidgets('favorites the eligible owned assets', (tester) async { final asset = owned(); - await tester.pumpTestAction(context, FavoriteAction(assets: [asset])); + await pumpFavorite(tester, {asset}); - verify(() => assetService.updateFavorite([asset.id], true)).called(1); + verify(() => assetService.update([asset.id], isFavorite: const Option.some(true))).called(1); }); testWidgets('unfavorite the eligible owned assets', (tester) async { final asset = owned(isFavorite: true); - await tester.pumpTestAction(context, FavoriteAction(assets: [asset])); + await pumpFavorite(tester, {asset}); - verify(() => assetService.updateFavorite([asset.id], false)).called(1); + verify(() => assetService.update([asset.id], isFavorite: const Option.some(false))).called(1); }); testWidgets('ignores assets owned by someone else', (tester) async { final mine = owned(); final theirs = RemoteAssetFactory.create(); - await tester.pumpTestAction(context, FavoriteAction(assets: [mine, theirs])); + await pumpFavorite(tester, {mine, theirs}); - verify(() => assetService.updateFavorite([mine.id], true)).called(1); - }); - - testWidgets('batches every eligible owned asset into a single call', (tester) async { - final first = owned(); - final second = owned(); - - await tester.pumpTestAction(context, FavoriteAction(assets: [first, second])); - - verify(() => assetService.updateFavorite([first.id, second.id], true)).called(1); + verify(() => assetService.update([mine.id], isFavorite: const Option.some(true))).called(1); }); testWidgets('skips owned assets already in the target state', (tester) async { final stale = owned(); final alreadyFavorite = owned(isFavorite: true); - await tester.pumpTestAction(context, FavoriteAction(assets: [stale, alreadyFavorite])); + await pumpFavorite(tester, {stale, alreadyFavorite}); - verify(() => assetService.updateFavorite([stale.id], true)).called(1); + verify(() => assetService.update([stale.id], isFavorite: const Option.some(true))).called(1); }); testWidgets('shows a confirmation snackbar on success', (tester) async { - await tester.pumpTestAction(context, FavoriteAction(assets: [owned()])); + await pumpFavorite(tester, {owned()}); await tester.pumpUntilFound(find.byType(SnackBar)); expect(find.byType(SnackBar), findsOneWidget); }); + + testWidgets('clears the selection once the update succeeds', (tester) async { + await pumpFavorite(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: FavoriteAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); }); } diff --git a/mobile/test/unit/presentation/actions/lock_action_test.dart b/mobile/test/unit/presentation/actions/lock_action_test.dart new file mode 100644 index 0000000000..9bad0f5a43 --- /dev/null +++ b/mobile/test/unit/presentation/actions/lock_action_test.dart @@ -0,0 +1,114 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() async { + await context.dispose(); + }); + + RemoteAsset owned({AssetVisibility visibility = .timeline}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility); + + Future pumpLock(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const LockAction(source: .timeline), overrides: context.selected(selection)); + + group('LockAction', () { + testWidgets('locks the eligible owned assets', (tester) async { + final asset = owned(); + + await pumpLock(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const .some(.locked))).called(1); + }); + + testWidgets('unlocks the eligible owned assets', (tester) async { + final asset = owned(visibility: .locked); + + await pumpLock(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const .some(.timeline))).called(1); + }); + + testWidgets('prioritizes lock when mixed state', (tester) async { + final unlocked = owned(); + final locked = owned(visibility: .locked); + + await pumpLock(tester, {unlocked, locked}); + + verify(() => assetService.update([unlocked.id], visibility: const .some(.locked))).called(1); + verifyNever(() => assetService.update(any(), visibility: const .some(.timeline))); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpLock(tester, {mine, theirs}); + + verify(() => assetService.update([mine.id], visibility: const .some(.locked))).called(1); + }); + + testWidgets('locks only the owned assets not already locked', (tester) async { + final stale = owned(); + final alreadyLocked = owned(visibility: .locked); + + await pumpLock(tester, {stale, alreadyLocked}); + + verify(() => assetService.update([stale.id], visibility: const .some(.locked))).called(1); + }); + + testWidgets('removes the local copies of the assets it locks', (tester) async { + final merged = RemoteAssetFactory.create(ownerId: context.currentUser.id, localId: 'local-1'); + final remoteOnly = owned(); + + await pumpLock(tester, {merged, remoteOnly}); + + verify(() => assetService.deleteLocal(['local-1'])).called(1); + }); + + testWidgets('leaves the local copies alone when unlocking', (tester) async { + final merged = RemoteAssetFactory.create( + ownerId: context.currentUser.id, + localId: 'local-1', + visibility: .locked, + ); + + await pumpLock(tester, {merged}); + + verifyNever(() => assetService.deleteLocal(any())); + }); + + testWidgets('clears the selection once the update succeeds', (tester) async { + await pumpLock(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: LockAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart b/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart new file mode 100644 index 0000000000..edd9ec4a7c --- /dev/null +++ b/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; +import 'package:immich_ui/immich_ui.dart'; + +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUp(() async { + context = await PresentationContext.create(); + }); + + tearDown(() async { + await context.dispose(); + }); + + group('webPathFor', () { + const dedicatedPages = { + TimelineOrigin.favorite: '/favorites', + TimelineOrigin.trash: '/trash', + TimelineOrigin.archive: '/archive', + }; + + for (final origin in TimelineOrigin.values) { + final expected = dedicatedPages[origin] ?? ''; + + test('opens ${origin.name} on ${expected.isEmpty ? 'the main timeline' : expected}', () { + expect(webPathFor(origin), expected); + }); + } + }); + + group('OpenInBrowserAction', () { + testWidgets('always renders, since the kebab menu decides whether to offer it', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: OpenInBrowserAction(remoteId: 'remote-1', origin: .main), + ), + ); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/partner_action_test.dart b/mobile/test/unit/presentation/actions/partner_action_test.dart index a1d645ffec..786d2d7f60 100644 --- a/mobile/test/unit/presentation/actions/partner_action_test.dart +++ b/mobile/test/unit/presentation/actions/partner_action_test.dart @@ -19,8 +19,8 @@ void main() { partnerService = context.service.partner.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); List overrides({List candidates = const []}) => [ diff --git a/mobile/test/unit/presentation/actions/restore_action_test.dart b/mobile/test/unit/presentation/actions/restore_action_test.dart new file mode 100644 index 0000000000..530e1b76e9 --- /dev/null +++ b/mobile/test/unit/presentation/actions/restore_action_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() async { + await context.dispose(); + }); + + RemoteAsset owned({bool trashed = true}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, deletedAt: trashed ? .new(2020) : null); + + Future pumpRestore(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const RestoreAction(source: .timeline), overrides: context.selected(selection)); + + group('RestoreAction', () { + testWidgets('restores the eligible owned trashed assets', (tester) async { + final asset = owned(); + + await pumpRestore(tester, {asset}); + + verify(() => assetService.restoreTrash([asset.id])).called(1); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(deletedAt: .new(2020)); + + await pumpRestore(tester, {mine, theirs}); + + verify(() => assetService.restoreTrash([mine.id])).called(1); + }); + + testWidgets('skips owned assets that are not trashed', (tester) async { + final trashed = owned(); + final live = owned(trashed: false); + + await pumpRestore(tester, {trashed, live}); + + verify(() => assetService.restoreTrash([trashed.id])).called(1); + }); + + testWidgets('clears the selection once the restore succeeds', (tester) async { + await pumpRestore(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when no owned asset is trashed', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: RestoreAction(source: .timeline)), + overrides: context.selected({owned(trashed: false)}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/share_action_test.dart b/mobile/test/unit/presentation/actions/share_action_test.dart new file mode 100644 index 0000000000..20cc25b73b --- /dev/null +++ b/mobile/test/unit/presentation/actions/share_action_test.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.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/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUp(() async { + context = await PresentationContext.create(); + }); + + tearDown(() async { + await context.dispose(); + }); + + late BuildContext actionContext; + late WidgetRef actionRef; + + Future pumpShare(WidgetTester tester, {Set? selection}) => tester.pumpTestWidget( + context, + Consumer( + builder: (widgetContext, ref, _) { + actionContext = widgetContext; + actionRef = ref; + return const ActionIconButton(action: ShareAction(source: .timeline)); + }, + ), + overrides: [ + ...context.selected(selection ?? {RemoteAssetFactory.create(ownerId: context.currentUser.id)}), + ], + ); + + // TODO: Replace with button tap once long press support in ui is merged + Future invokeSecondaryAction() async { + final resolved = const ShareAction(source: .timeline).create(actionContext, actionRef); + await resolved!.onSecondaryAction!(); + } + + List sharedFileTypes() => verify( + () => context.repository.assetMedia.api.shareAssets( + any(), + any(), + fileType: captureAny(named: 'fileType'), + cancelCompleter: any(named: 'cancelCompleter'), + onAssetDownloadProgress: any(named: 'onAssetDownloadProgress'), + ), + ).captured.cast(); + + Future settle(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const .new(milliseconds: 300)); + } + + Future pickPreviewQuality(WidgetTester tester) async { + final shared = invokeSecondaryAction(); + await settle(tester); + await tester.tap(find.byIcon(Icons.photo_size_select_large_rounded)); + await settle(tester); + await shared; + } + + group('ShareAction', () { + testWidgets('single press shares with the configured default quality', (tester) async { + await pumpShare(tester); + + await tester.tap(find.byType(ImmichIconButton)); + await settle(tester); + + expect(sharedFileTypes(), [ShareAssetType.original]); + }); + + testWidgets('the secondary action shares with the quality picked in the dialog', (tester) async { + await pumpShare(tester); + + await pickPreviewQuality(tester); + + expect(sharedFileTypes(), [ShareAssetType.preview]); + }); + + testWidgets('quality picked there is a one-time choice and does not change the default', (tester) async { + await pumpShare(tester); + + await pickPreviewQuality(tester); + await tester.tap(find.byType(ImmichIconButton)); + await settle(tester); + + expect(sharedFileTypes(), [ShareAssetType.preview, ShareAssetType.original]); + expect(SettingsRepository.instance.appConfig.share.fileType, ShareAssetType.original); + }); + + testWidgets('offers no preview option for a video, which has none to share', (tester) async { + await pumpShare( + tester, + selection: {RemoteAssetFactory.create(ownerId: context.currentUser.id, type: .video)}, + ); + + final shared = invokeSecondaryAction(); + await settle(tester); + + expect(find.byIcon(Icons.high_quality_rounded), findsOneWidget); + expect(find.byIcon(Icons.photo_size_select_large_rounded), findsNothing); + + await tester.tap(find.byIcon(Icons.high_quality_rounded)); + await settle(tester); + await shared; + expect(find.byIcon(Icons.photo_size_select_large_rounded), findsNothing); + }); + + testWidgets('is hidden when nothing is selected', (tester) async { + await pumpShare(tester, selection: const {}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('ShareLinkAction', () { + testWidgets('offers a link for a remote asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ShareLinkAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden for a local-only asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ShareLinkAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/stack_action_test.dart b/mobile/test/unit/presentation/actions/stack_action_test.dart new file mode 100644 index 0000000000..11321c28af --- /dev/null +++ b/mobile/test/unit/presentation/actions/stack_action_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() async { + await context.dispose(); + }); + + RemoteAsset owned({String? stackId}) => RemoteAssetFactory.create(ownerId: context.currentUser.id, stackId: stackId); + + Future pumpStack(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const StackAction(source: .timeline), overrides: context.selected(selection)); + + group('StackAction', () { + testWidgets('stacks the eligible owned assets', (tester) async { + final first = owned(); + final second = owned(); + + await pumpStack(tester, {first, second}); + + verify(() => assetService.stack(context.currentUser.id, [first.id, second.id])).called(1); + }); + + testWidgets('unstacks the eligible owned assets', (tester) async { + final asset = owned(stackId: 'stack'); + + await pumpStack(tester, {asset}); + + verify(() => assetService.unstack(['stack'])).called(1); + }); + + testWidgets('prioritizes stack when mixed state', (tester) async { + final first = owned(); + final second = owned(stackId: 'stack'); + + await pumpStack(tester, {first, second}); + + verify(() => assetService.stack(context.currentUser.id, [first.id, second.id])).called(1); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final other = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpStack(tester, {mine, other, theirs}); + + verify(() => assetService.stack(context.currentUser.id, [mine.id, other.id])).called(1); + }); + + testWidgets('clears the selection once the stack succeeds', (tester) async { + await pumpStack(tester, {owned(), owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when a unstacked asset has nothing to stack onto', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: StackAction(source: .timeline)), + overrides: context.selected({owned()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: StackAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create(), RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/timeline_action_test.dart b/mobile/test/unit/presentation/actions/timeline_action_test.dart deleted file mode 100644 index 5661be72c0..0000000000 --- a/mobile/test/unit/presentation/actions/timeline_action_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/actions/action.dart'; -import 'package:immich_mobile/presentation/actions/action.widget.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; - -import '../../factories/remote_asset_factory.dart'; -import '../presentation_context.dart'; - -class _FakeAction extends BaseAction { - _FakeAction({this.visible = true, this.error}); - - final bool visible; - final Object? error; - - bool ran = false; - bool? selectionDuringOnAction; - - @override - IconData get icon => Icons.bolt; - - @override - String label(ActionScope scope) => 'fake'; - - @override - bool isVisible(ActionScope scope) => visible; - - @override - Future onAction(ActionScope scope) async { - ran = true; - selectionDuringOnAction = scope.ref.read(multiSelectProvider).isEnabled; - if (error != null) { - throw error!; - } - } -} - -void main() { - late PresentationContext context; - - setUp(() async { - context = await PresentationContext.create(); - }); - - tearDown(() { - context.dispose(); - }); - - List overrides() => [ - multiSelectProvider.overrideWith( - () => MultiSelectNotifier( - MultiSelectState(selectedAssets: {RemoteAssetFactory.create()}, lockedSelectionAssets: const {}), - ), - ), - ]; - - Future<(ActionScope, ProviderContainer)> pumpScope(WidgetTester tester) async { - late ActionScope scope; - late ProviderContainer container; - await tester.pumpTestWidget( - context, - Consumer( - builder: (innerContext, ref, _) { - scope = ActionScope(context: innerContext, ref: ref, authUser: context.currentUser); - container = ProviderScope.containerOf(innerContext, listen: false); - return const SizedBox.shrink(); - }, - ), - overrides: overrides(), - ); - return (scope, container); - } - - group('TimelineAction', () { - testWidgets('runs the wrapped action and then clears the selection', (tester) async { - final inner = _FakeAction(); - final (scope, container) = await pumpScope(tester); - await TimelineAction(action: inner).onAction(scope); - - expect(inner.ran, isTrue); - expect(inner.selectionDuringOnAction, isTrue, reason: 'reset must run after the inner action, not before'); - expect(container.read(multiSelectProvider).isEnabled, isFalse); - }); - - testWidgets('rethrows and keeps the selection when the wrapped action throws', (tester) async { - final error = Exception('boom'); - final inner = _FakeAction(error: error); - final (scope, container) = await pumpScope(tester); - - await expectLater(TimelineAction(action: inner).onAction(scope), throwsA(same(error))); - - expect(inner.ran, isTrue); - expect(container.read(multiSelectProvider).isEnabled, isTrue); - }); - - testWidgets('delegates visibility to the wrapped action', (tester) async { - await tester.pumpTestWidget( - context, - ActionIconButtonWidget(action: TimelineAction(action: _FakeAction(visible: false))), - ); - - expect(find.byType(ActionIconButtonWidget), findsOneWidget); - expect(find.byIcon(Icons.bolt), findsNothing); - }); - }); -} diff --git a/mobile/test/unit/presentation/actions/upload_action_test.dart b/mobile/test/unit/presentation/actions/upload_action_test.dart new file mode 100644 index 0000000000..8aeb7a915f --- /dev/null +++ b/mobile/test/unit/presentation/actions/upload_action_test.dart @@ -0,0 +1,213 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.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/upload.action.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockForegroundUploadService uploadService; + + setUp(() async { + context = await PresentationContext.create(); + uploadService = context.service.upload; + }); + + tearDown(() async { + await context.dispose(); + }); + + List uploadOverrides() => [ + foregroundUploadServiceProvider.overrideWithValue(uploadService), + toastServiceProvider.overrideWithValue(context.service.toast), + ]; + + Future pumpUpload(WidgetTester tester, Set selection, {bool showProgress = false}) => + tester.pumpTestWidget( + context, + ActionIconButton( + action: UploadAction(source: .timeline, showProgress: showProgress), + ), + overrides: [...context.selected(selection), ...uploadOverrides()], + ); + + Future settleUpload(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const .new(seconds: 2)); + await tester.pumpAndSettle(); + } + + void answerUploadWith({Set succeeded = const {}, Set failed = const {}, Completer? until}) { + when( + () => uploadService.uploadManual( + any(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).thenAnswer((invocation) async { + if (until != null) { + await until.future; + } + final callbacks = invocation.namedArguments[#callbacks] as UploadCallbacks; + for (final id in succeeded) { + callbacks.onSuccess?.call(id, id); + } + for (final id in failed) { + callbacks.onError?.call(id, 'boom'); + } + }); + } + + group('UploadAction', () { + testWidgets('uploads the selected local assets', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(succeeded: {asset.id}); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + final uploaded = + verify( + () => uploadService.uploadManual( + captureAny(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).captured.single + as List; + expect(uploaded.map((a) => a.id), [asset.id]); + }); + + testWidgets('ignores assets that are already backed up', (tester) async { + final notBackedUp = LocalAssetFactory.create(); + answerUploadWith(succeeded: {notBackedUp.id}); + + await pumpUpload(tester, {notBackedUp, LocalAssetFactory.create(remoteId: 'already-there')}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + final uploaded = + verify( + () => uploadService.uploadManual( + captureAny(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).captured.single + as List; + expect(uploaded.map((a) => a.id), [notBackedUp.id]); + }); + + testWidgets('reports an error when an asset fails to upload', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(failed: {asset.id}); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + final message = verify(() => context.service.toast.error(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.scaffold_body_error_occurred); + }); + + testWidgets('treats a cancelled upload as deliberate, not a failure', (tester) async { + final asset = LocalAssetFactory.create(); + when( + () => uploadService.uploadManual( + any(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).thenAnswer((invocation) async { + (invocation.namedArguments[#cancelToken] as Completer).complete(); + }); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + verifyNever(() => context.service.toast.error(any())); + }); + + testWidgets('shows the progress dialog while uploading and closes it after', (tester) async { + final asset = LocalAssetFactory.create(); + final uploading = Completer(); + answerUploadWith(succeeded: {asset.id}, until: uploading); + + await pumpUpload(tester, {asset}, showProgress: true); + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + + expect(find.text(StaticTranslations.instance.uploading), findsOneWidget); + + uploading.complete(); + await settleUpload(tester); + + expect(find.text(StaticTranslations.instance.uploading), findsNothing); + }); + + testWidgets('shows no dialog when not asked to', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(succeeded: {asset.id}); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + + expect(find.text(StaticTranslations.instance.uploading), findsNothing); + await settleUpload(tester); + }); + + testWidgets('is hidden for a remote asset, which has nothing to upload', (tester) async { + await pumpUpload(tester, {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden when every local asset is already backed up', (tester) async { + await pumpUpload(tester, {LocalAssetFactory.create(remoteId: 'already-there')}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('uploadAssets', () { + testWidgets('clears the tracked progress once the upload settles', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(succeeded: {asset.id}); + + late WidgetRef capturedRef; + await tester.pumpTestWidget( + context, + Consumer( + builder: (_, ref, _) { + capturedRef = ref; + return const SizedBox.shrink(); + }, + ), + overrides: uploadOverrides(), + ); + + await uploadAssets(tester.element(find.byType(SizedBox)), capturedRef, [asset]); + await settleUpload(tester); + + expect(capturedRef.read(assetUploadProgressProvider), isEmpty); + expect(capturedRef.read(manualUploadCancelTokenProvider), isNull); + }); + }); +} diff --git a/mobile/test/unit/presentation/partner_page_test.dart b/mobile/test/unit/presentation/partner_page_test.dart index 162094c720..2cabbb9d4c 100644 --- a/mobile/test/unit/presentation/partner_page_test.dart +++ b/mobile/test/unit/presentation/partner_page_test.dart @@ -3,7 +3,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/pages/library/partner/partner.page.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/partner.action.dart'; +import 'package:immich_ui/immich_ui.dart'; import '../factories/partner_user_factory.dart'; import '../factories/user_factory.dart'; @@ -13,16 +15,14 @@ void main() { late PresentationContext context; setUp(() async => context = await PresentationContext.create()); - tearDown(() => context.dispose()); + tearDown(() async => await context.dispose()); group('PartnerSharedByList', () { testWidgets('shows the empty-state add button when there are no partners', (tester) async { - final action = const PartnerAddAction(); - await tester.pumpTestWidget(context, const PartnerSharedByList(partners: [])); expect(find.byType(ListView), findsNothing); - expect(find.widgetWithIcon(TextButton, action.icon), findsOneWidget); + expect(find.descendant(of: find.byType(ActionButton), matching: find.byType(ImmichTextButton)), findsOneWidget); }); testWidgets('renders a tile per partner with name and email', (tester) async { @@ -39,9 +39,11 @@ void main() { testWidgets('renders a remove action for each partner', (tester) async { final partner1 = PartnerFactory.create(inTimeline: true); final partner2 = PartnerFactory.create(); - final action = const PartnerRemoveAction(sharedWithId: '', partnerName: ''); await tester.pumpTestWidget(context, PartnerSharedByList(partners: [partner1, partner2])); - expect(find.byIcon(action.icon), findsNWidgets(2)); + expect( + find.descendant(of: find.byType(ActionIconButton), matching: find.byType(ImmichIconButton)), + findsNWidgets(2), + ); }); }); diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index 25de583049..36ae8d087d 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -5,18 +5,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/locales.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/services/cleanup.service.dart'; import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; import 'package:immich_ui/immich_ui.dart'; @@ -32,7 +38,6 @@ class PresentationContext { service = ServiceMocks(), repository = RepositoryMocks() { setup(); - addTearDown(dispose); } static const String serverEndpoint = 'http://localhost:3000'; @@ -46,10 +51,20 @@ class PresentationContext { List get overrides => [ currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)), assetServiceProvider.overrideWithValue(service.asset.service), + cleanupServiceProvider.overrideWithValue(service.cleanup.service), + remoteAlbumServiceProvider.overrideWithValue(service.album.service), partnerServiceProvider.overrideWithValue(service.partner.service), gCastServiceProvider.overrideWithValue(service.cast), serverInfoServiceProvider.overrideWithValue(service.serverInfo), inLockedViewProvider.overrideWithValue(false), + remoteAssetRepositoryProvider.overrideWithValue(repository.remoteAsset.repo), + assetMediaRepositoryProvider.overrideWithValue(repository.assetMedia.api), + ]; + + List selected(Set assets) => [ + multiSelectProvider.overrideWith( + () => MultiSelectNotifier(MultiSelectState(selectedAssets: assets, lockedSelectionAssets: const {})), + ), ]; static Future create() async { @@ -60,6 +75,7 @@ class PresentationContext { await StoreService.I.put(StoreKey.serverEndpoint, serverEndpoint); _db = db; } + await SettingsRepository.ensureInitialized(_db!); return PresentationContext._(user: UserFactory.createDto()); } @@ -67,7 +83,8 @@ class PresentationContext { when(service.user.tryGetMyUser).thenReturn(currentUser); } - void dispose() { + Future dispose() async { + await SettingsRepository.reset(); service.resetAll(); } } @@ -84,15 +101,18 @@ extension PumpPresentationWidget on WidgetTester { useFallbackTranslations: true, assetLoader: const CodegenLoader(), child: ProviderScope( - overrides: [...context.overrides, ...overrides], + overrides: context.overrides, child: Builder( - builder: (context) => MaterialApp( - debugShowCheckedModeBanner: false, - scaffoldMessengerKey: scaffoldMessengerKey, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - home: Scaffold(body: widget), + builder: (context) => ProviderScope( + overrides: overrides, + child: MaterialApp( + debugShowCheckedModeBanner: false, + scaffoldMessengerKey: scaffoldMessengerKey, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + home: Scaffold(body: widget), + ), ), ), ), @@ -103,10 +123,10 @@ extension PumpPresentationWidget on WidgetTester { Future pumpTestAction( PresentationContext context, - BaseAction action, { + ActionBuilder action, { List overrides = const [], }) async { - await pumpTestWidget(context, ActionIconButtonWidget(action: action), overrides: overrides); + await pumpTestWidget(context, ActionIconButton(action: action), overrides: overrides); await tap(find.byType(ImmichIconButton)); await pump(); } diff --git a/mobile/test/unit/services/asset_service_test.dart b/mobile/test/unit/services/asset_service_test.dart index 5e473d8f66..a09465701b 100644 --- a/mobile/test/unit/services/asset_service_test.dart +++ b/mobile/test/unit/services/asset_service_test.dart @@ -24,6 +24,8 @@ void main() { exifRepository: exifRepository, localRepository: MockDriftLocalAssetRepository(), apiRepository: apiRepository, + mediaRepository: mocks.assetMedia.api, + trashedLocalRepository: mocks.trashedAsset, ); }); diff --git a/mobile/test/unit/utils/editor_test.dart b/mobile/test/unit/utils/editor_test.dart index 82cf584f76..6f3f41f374 100644 --- a/mobile/test/unit/utils/editor_test.dart +++ b/mobile/test/unit/utils/editor_test.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/utils/editor.utils.dart'; import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; List normalizedToEdits(NormalizedTransform transform) { - List edits = []; + final List edits = []; if (transform.mirrorHorizontal) { edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); @@ -25,10 +25,10 @@ bool compareEditAffines(List editsA, List editsB) { final normA = buildAffineFromEdits(editsA); final normB = buildAffineFromEdits(editsB); - return ((normA.a - normB.a).abs() < 0.0001 && + return (normA.a - normB.a).abs() < 0.0001 && (normA.b - normB.b).abs() < 0.0001 && (normA.c - normB.c).abs() < 0.0001 && - (normA.d - normB.d).abs() < 0.0001); + (normA.d - normB.d).abs() < 0.0001; } void main() { diff --git a/mobile/test/utils_legacy/action_button_utils_test.dart b/mobile/test/utils_legacy/action_button_utils_test.dart index 0a6020762a..65e1cad7f7 100644 --- a/mobile/test/utils_legacy/action_button_utils_test.dart +++ b/mobile/test/utils_legacy/action_button_utils_test.dart @@ -427,60 +427,6 @@ void main() { }); }); - group('trash button', () { - test('should show when owner, not locked, has remote, and trash enabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.trash.shouldShow(context), isTrue); - }); - - test('should not show when trash disabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: false, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.trash.shouldShow(context), isFalse); - }); - - test('should not show when asset is already trashed', () { - final remoteAsset = createRemoteAsset(deletedAt: DateTime(2024)); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.viewer, - timelineOrigin: TimelineOrigin.trash, - ); - - expect(ActionButtonType.trash.shouldShow(context), isFalse); - }); - }); - group('restoreTrash button', () { test('should show when owner, not locked, has remote, and is in trash timeline', () { final remoteAsset = createRemoteAsset(); @@ -519,60 +465,6 @@ void main() { }); }); - group('deletePermanent button', () { - test('should show when owner, not locked, has remote, and trash disabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: false, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.deletePermanent.shouldShow(context), isTrue); - }); - - test('should not show when trash enabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.deletePermanent.shouldShow(context), isFalse); - }); - - test('should show when asset is trashed even with trash enabled', () { - final remoteAsset = createRemoteAsset(deletedAt: DateTime(2024)); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.viewer, - timelineOrigin: TimelineOrigin.trash, - ); - - expect(ActionButtonType.deletePermanent.shouldShow(context), isTrue); - }); - }); - group('delete button', () { test('should show when owner, not locked, and has remote', () { final remoteAsset = createRemoteAsset(); @@ -612,7 +504,7 @@ void main() { }); group('deleteLocal button', () { - test('should show when not locked and asset is local only', () { + test('should not show when asset is local only, as there is no backup to fall back on', () { final localAsset = createLocalAsset(); final context = ActionButtonContext( asset: localAsset, @@ -626,7 +518,7 @@ void main() { source: ActionSource.timeline, ); - expect(ActionButtonType.deleteLocal.shouldShow(context), isTrue); + expect(ActionButtonType.deleteLocal.shouldShow(context), isFalse); }); test('should not show when asset is not local only', () { @@ -1074,7 +966,7 @@ void main() { test('should build correct widget for each button type', () { for (final buttonType in ActionButtonType.values) { - var buttonContext = context; + final buttonContext = context; if (buttonType == ActionButtonType.removeFromAlbum) { final album = createRemoteAlbum(); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index dd5d4dce45..bc3bf82094 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -16585,7 +16585,18 @@ }, "description": { "description": "Album description", - "type": "string" + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "An empty string is returned instead of null for backwards compatibility; null will be returned in v4." + } + ] }, "endDate": { "description": "End date (latest asset)", @@ -18443,7 +18454,19 @@ }, "description": { "description": "Album description", - "type": "string" + "nullable": true, + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." + } + ] } }, "required": [ @@ -27115,7 +27138,19 @@ }, "description": { "description": "Album description", - "type": "string" + "nullable": true, + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." + } + ] }, "isActivityEnabled": { "description": "Enable activity feed", diff --git a/package.json b/package.json index 817988a100..2464012cee 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.13.1", + "packageManager": "pnpm@11.17.0", "engines": { "pnpm": ">=10.0.0" }, diff --git a/packages/cli/Dockerfile b/packages/cli/Dockerfile index ee2a4294bd..716522ecd7 100644 --- a/packages/cli/Dockerfile +++ b/packages/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 AS core +FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 AS core WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ diff --git a/packages/cli/mise.toml b/packages/cli/mise.toml index 28d5e1858f..320ce001d8 100644 --- a/packages/cli/mise.toml +++ b/packages/cli/mise.toml @@ -29,16 +29,18 @@ env._.path = "./node_modules/.bin" run = "tsc --noEmit" [tasks.ci-publish] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":build" }, "pnpm publish --provenance --no-git-checks", ] [tasks.ci-unit] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":format" }, { task = ":lint" }, diff --git a/packages/e2e-auth-server/Dockerfile b/packages/e2e-auth-server/Dockerfile index 3a49307316..bfc939c084 100644 --- a/packages/e2e-auth-server/Dockerfile +++ b/packages/e2e-auth-server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 +FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 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 5e88465de4..b872dfe36d 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.13.1" + "packageManager": "pnpm@11.17.0" } diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index e3f180f98b..7b91ed7111 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -89,7 +89,8 @@ const methods = wrapper({ } }, - assetFileFilter: ({ data, config }) => matchValueResult(data.asset.originalFileName || '', config), + assetFileFilter: ({ data, config }) => + matchValueResult(config.usePath ? data.asset.originalPath : data.asset.originalFileName, config), assetLocationFilter: ({ config, data }) => { if ( diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 51fee7efda..8afa00cfb4 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.13.1", + "packageManager": "pnpm@11.17.0", "devDependencies": { "@extism/js-pdk": "^1.1.1", "@immich/sdk": "workspace:*", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index c2128db479..66327e6150 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.13.1" + "packageManager": "pnpm@11.17.0" } diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index a7e0d9511e..3ac958ce2d 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -540,7 +540,7 @@ export type CreateAlbumDto = { /** Initial asset IDs */ assetIds?: string[]; /** Album description */ - description?: string; + description?: string | null; }; export type AlbumsAddAssetsDto = { /** Album IDs */ @@ -567,7 +567,7 @@ export type UpdateAlbumDto = { /** Album thumbnail asset ID */ albumThumbnailAssetId?: string; /** Album description */ - description?: string; + description?: string | null; /** Enable activity feed */ isActivityEnabled?: boolean; order?: AssetOrder; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4d3e4246a..44d920aaa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,31 +19,31 @@ importers: devDependencies: prettier: specifier: ^3.8.3 - version: 3.9.5 + version: 3.9.6 prettier-plugin-sort-json: specifier: ^4.2.0 - version: 4.2.0(prettier@3.9.5) + version: 4.2.0(prettier@3.9.6) .github: devDependencies: prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 docs: 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + 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) '@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.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) + 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) '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + 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) '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + 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) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -52,31 +52,31 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.17)(react@19.2.7) + version: 3.1.1(@types/react@19.2.17)(react@19.2.8) autoprefixer: specifier: ^10.4.17 - version: 10.5.4(postcss@8.5.19) + version: 10.5.4(postcss@8.5.25) 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + 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) lunr: specifier: ^2.3.9 version: 2.3.9 postcss: specifier: ^8.4.25 - version: 8.5.19 + version: 8.5.25 prism-react-renderer: specifier: ^2.3.1 - version: 2.4.1(react@19.2.7) + version: 2.4.1(react@19.2.8) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.108.4(postcss@8.5.19)) + 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)) react: specifier: ^19.0.0 - version: 19.2.7 + version: 19.2.8 react-dom: specifier: ^19.0.0 - version: 19.2.7(react@19.2.7) + 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) @@ -86,19 +86,19 @@ importers: devDependencies: '@docusaurus/module-type-aliases': specifier: ~3.10.0 - version: 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + 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) '@docusaurus/tsconfig': specifier: ^3.10.0 version: 3.10.2 '@docusaurus/types': specifier: ^3.10.0 - version: 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + 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) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' @@ -107,7 +107,7 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@faker-js/faker': specifier: ^10.1.0 version: 10.5.0 @@ -122,7 +122,7 @@ importers: version: link:../packages/sdk '@playwright/test': specifier: ^1.44.1 - version: 1.61.1 + version: 1.62.0 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 @@ -149,16 +149,16 @@ importers: version: 17.4.2 eslint: specifier: ^10.0.0 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) + version: 5.5.6(@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@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) exiftool-vendored: specifier: ^35.0.0 version: 35.21.0 @@ -176,31 +176,31 @@ importers: version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5) + version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6) sharp: specifier: ^0.34.5 version: 0.34.5 socket.io-client: specifier: ^4.7.4 - version: 4.8.3 + version: 4.8.3(supports-color@8.1.1) supertest: specifier: ^7.0.0 - version: 7.2.2 + version: 7.2.2(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.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.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) utimes: specifier: ^5.2.1 - version: 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.10.6)(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.101.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.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)) packages/cli: dependencies: @@ -222,7 +222,7 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@immich/sdk': specifier: workspace:* version: link:../sdk @@ -261,16 +261,16 @@ importers: version: 15.0.0 eslint: specifier: ^10.0.0 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) + version: 5.5.6(@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@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + 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 @@ -279,22 +279,22 @@ importers: version: 5.5.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5) + version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.58.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.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) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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) 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.10.6)(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.101.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.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-fetch-mock: specifier: ^0.4.0 version: 0.4.5(vitest@4.1.10) @@ -309,10 +309,10 @@ importers: version: 9.5.0 jose: specifier: ^6.0.0 - version: 6.2.3 + version: 6.2.4 oidc-provider: specifier: ^9.0.0 - version: 9.9.1 + version: 9.10.0(supports-color@8.1.1) tsx: specifier: ^4.20.6 version: 4.23.1 @@ -377,10 +377,10 @@ importers: version: 7.7.1 vite: specifier: ^8.0.16 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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) 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.10.6)(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.101.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.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)) packages/sdk: dependencies: @@ -408,70 +408,70 @@ 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))(@nestjs/core@11.1.28)(bullmq@5.80.5) + 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)) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/core': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 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) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + 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)(supports-color@8.1.1) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2) + version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.1.3(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + version: 6.1.3(@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/swagger': specifier: ^11.4.2 - version: 11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2) + version: 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) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.1 '@opentelemetry/context-async-hooks': specifier: ^2.0.0 - version: 2.9.0(@opentelemetry/api@1.9.1) + version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/exporter-prometheus': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) + specifier: ^0.221.0 + version: 0.221.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation-http': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) + specifier: ^0.221.0 + version: 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/instrumentation-ioredis': - specifier: ^0.68.0 - version: 0.68.0(@opentelemetry/api@1.9.1) + specifier: ^0.69.0 + version: 0.69.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.66.0 - version: 0.66.0(@opentelemetry/api@1.9.1) + specifier: ^0.67.0 + version: 0.67.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/instrumentation-pg': - specifier: ^0.72.0 - version: 0.72.0(@opentelemetry/api@1.9.1) + specifier: ^0.73.0 + version: 0.73.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.9.0(@opentelemetry/api@1.9.1) + version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.9.0(@opentelemetry/api@1.9.1) + version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-node': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) + specifier: ^0.221.0 + version: 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.43.0 '@react-email/components': specifier: ^1.0.0 - version: 1.0.12(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 1.0.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@react-email/render': specifier: ^2.0.0 - version: 2.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 2.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@socket.io/redis-adapter': specifier: ^8.3.0 - version: 8.3.0(socket.io-adapter@2.5.7) + version: 8.3.0(socket.io-adapter@2.5.7(supports-color@8.1.1))(supports-color@8.1.1) archiver: specifier: ^7.0.0 version: 7.0.1 @@ -483,16 +483,16 @@ importers: version: 6.0.0 body-parser: specifier: ^2.2.0 - version: 2.3.0 + version: 2.3.0(supports-color@8.1.1) bullmq: specifier: ^5.51.0 - version: 5.80.5 + version: 5.81.2(supports-color@8.1.1) chokidar: specifier: ^4.0.3 version: 4.0.3 compression: specifier: ^1.8.0 - version: 1.8.1 + version: 1.8.1(supports-color@8.1.1) cookie: specifier: ^1.0.2 version: 1.1.1 @@ -507,7 +507,7 @@ importers: version: 35.21.0 express: specifier: ^5.1.0 - version: 5.2.1 + version: 5.2.1(supports-color@8.1.1) fast-glob: specifier: ^3.3.2 version: 3.3.3 @@ -531,13 +531,13 @@ importers: version: 7.14.0 ioredis: specifier: ^5.8.2 - version: 5.11.1 + version: 5.11.1(supports-color@8.1.1) jose: specifier: ^6.0.0 - version: 6.2.3 + version: 6.2.4 js-yaml: - specifier: ^4.1.0 - version: 4.3.0 + specifier: ^5.0.0 + version: 5.2.2 jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 @@ -561,19 +561,19 @@ importers: version: 2.2.0 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2) + version: 3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2) nestjs-cls: specifier: ^6.0.0 - version: 6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^8.0.0 - version: 8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(rxjs@7.8.2) + version: 8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(rxjs@7.8.2) nestjs-zod: specifier: ^5.3.0 - version: 5.4.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + 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 @@ -591,13 +591,13 @@ importers: version: 3.4.9 react: specifier: ^19.0.0 - version: 19.2.7 + version: 19.2.8 react-dom: specifier: ^19.0.0 - version: 19.2.7(react@19.2.7) + version: 19.2.8(react@19.2.8) react-email: specifier: ^5.0.0 - version: 5.2.11 + version: 5.2.11(supports-color@8.1.1) reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -618,7 +618,7 @@ importers: version: 3.0.2 socket.io: specifier: ^4.8.1 - version: 4.8.3 + 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)) @@ -643,19 +643,19 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + 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.43(@swc/helpers@0.5.23))(@types/node@24.13.3)(esbuild@0.28.1)(lightningcss@1.33.0)(prettier@3.9.5) + 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) '@nestjs/schematics': specifier: ^11.0.0 - version: 11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.5) + version: 11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.6) '@nestjs/testing': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28) + 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.43(@swc/helpers@0.5.23) + version: 1.15.46(@swc/helpers@0.5.23) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -730,19 +730,19 @@ 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.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(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.1)(yaml@2.9.0)) eslint: specifier: ^10.0.0 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) + version: 5.5.6(@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@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + 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 @@ -754,37 +754,37 @@ importers: version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5) + version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6) sql-formatter: specifier: ^15.0.0 version: 15.8.2 supertest: specifier: ^7.1.0 - version: 7.2.2 + 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) testcontainers: specifier: ^12.0.0 - version: 12.0.4 + version: 12.0.4(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.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.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) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.43(@swc/helpers@0.5.23))(rollup@4.62.0) + version: 1.5.9(@swc/core@1.15.46(@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)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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: specifier: ^3.0.0 - version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(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.1)(yaml@2.9.0) web: dependencies: @@ -799,7 +799,7 @@ importers: version: link:../packages/sdk '@immich/ui': specifier: ^0.83.0 - version: 0.83.0(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.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)) '@mapbox/mapbox-gl-rtl-text': specifier: 0.4.0 version: 0.4.0 @@ -811,22 +811,22 @@ importers: version: 2.2.0 '@photo-sphere-viewer/core': specifier: ^5.14.0 - version: 5.14.3 + version: 5.15.0 '@photo-sphere-viewer/equirectangular-video-adapter': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)) + 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)) '@photo-sphere-viewer/markers-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0) '@photo-sphere-viewer/resolution-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/settings-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)) + 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)) '@photo-sphere-viewer/settings-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0) '@photo-sphere-viewer/video-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 @@ -835,13 +835,13 @@ importers: version: 0.42.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.9(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0)) dom-to-image: specifier: ^2.6.0 version: 2.6.0 fabric: specifier: ^7.0.0 - version: 7.4.0 + version: 7.4.0(supports-color@8.1.1) geo-coordinates-parser: specifier: ^1.7.4 version: 1.7.4 @@ -853,7 +853,7 @@ importers: version: 4.7.9 happy-dom: specifier: ^20.0.0 - version: 20.10.6 + version: 20.11.1 hls-video-element: specifier: ^1.5.11 version: 1.5.11 @@ -877,7 +877,7 @@ importers: version: 5.24.0 media-chrome: specifier: ^4.19.0 - version: 4.19.2(react@19.2.7) + version: 4.19.2(react@19.2.8) pmtiles: specifier: ^4.3.0 version: 4.4.1 @@ -886,25 +886,25 @@ importers: version: 1.5.4 simple-icons: specifier: ^16.0.0 - version: 16.26.0 + version: 16.27.1 socket.io-client: specifier: ~4.8.0 - version: 4.8.3 + version: 4.8.3(supports-color@8.1.1) svelte-gestures: specifier: ^5.2.2 version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-jsoneditor: specifier: ^3.10.0 - version: 3.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-maplibre: specifier: ^1.2.5 - version: 1.3.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.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.2) + version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) thumbhash: specifier: ^0.1.1 version: 0.1.1 @@ -926,43 +926,43 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@faker-js/faker': specifier: ^10.0.0 version: 10.5.0 '@koddsson/eslint-plugin-tscompat': specifier: ^0.2.0 - version: 0.2.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 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) '@socket.io/component-emitter': specifier: ^3.1.0 version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))) + 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))) '@sveltejs/enhanced-img': specifier: ^0.11.0 - version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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/kit': specifier: ^2.56.1 - version: 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + 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)) '@sveltejs/vite-plugin-svelte': specifier: 7.2.0 - version: 7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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)) '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.3.2(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + 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)) '@testing-library/jest-dom': - specifier: ^6.4.2 - version: 6.9.1 + 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.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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/user-event': specifier: ^14.5.2 version: 14.6.1(@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.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)))(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + 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)) '@types/chromecast-caf-sender': specifier: ^1.0.11 version: 1.0.11 @@ -992,22 +992,22 @@ importers: version: 17.4.2 eslint: specifier: ^10.2.1 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-better-tailwindcss: specifier: ^4.5.0 - version: 4.6.1(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))(tailwindcss@4.3.2) + version: 4.7.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(tailwindcss@4.3.3) eslint-plugin-compat: specifier: ^7.0.0 - version: 7.0.2(eslint@10.7.0(jiti@2.7.0)) + 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.20.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.5(@typescript-eslint/types@8.64.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.65.0)) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) factory.ts: specifier: ^1.4.1 version: 1.4.2 @@ -1016,40 +1016,40 @@ importers: version: 17.7.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.2.0(prettier@3.9.5) + version: 4.2.0(prettier@3.9.6) prettier-plugin-svelte: specifier: ^4.0.0 - version: 4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) rollup-plugin-visualizer: specifier: ^7.0.0 version: 7.0.1(rolldown@1.1.5)(rollup@4.62.0) svelte: - specifier: 5.56.5 - version: 5.56.5(@typescript-eslint/types@8.64.0) + specifier: 5.56.8 + version: 5.56.8(@typescript-eslint/types@8.65.0) svelte-check: specifier: ^4.4.6 - version: 4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.8.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) tailwindcss: specifier: ^4.2.4 - version: 4.3.2 + version: 4.3.3 typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.45.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.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) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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) 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.10.6)(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.101.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.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)) packages: @@ -1788,26 +1788,26 @@ packages: '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} - '@codemirror/commands@6.10.3': - resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} '@codemirror/lang-json@6.0.2': resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} - '@codemirror/language@6.12.3': - resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} '@codemirror/lint@6.9.7': resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} - '@codemirror/search@6.7.0': - resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} - '@codemirror/state@6.6.0': - resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} - '@codemirror/view@6.43.1': - resolution: {integrity: sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==} + '@codemirror/view@6.43.7': + resolution: {integrity: sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -2795,8 +2795,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -2809,16 +2809,16 @@ packages: resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/css-tree@4.0.4': - resolution: {integrity: sha512-nxMparyhqVWQvadx9x8dIfubfIPOE+X2b2waua8fzdnM9vdp9rgVtwEZlG0TmCwEUz/d/f40fzvO/eqBwdxz0A==} + '@eslint/css-tree@4.0.5': + resolution: {integrity: sha512-iPmijIAq4hlIJB86PYmY/fcZORHtjphSqICDbwuw32A/JmkhZQ/K/6TjHE03zqf3n5yABpVcbRAMG8Mi9ojy8g==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/js@10.0.1': @@ -2853,14 +2853,14 @@ packages: peerDependencies: commander: ^11.1.0 - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} @@ -2886,16 +2886,16 @@ packages: '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - '@fortawesome/fontawesome-common-types@7.2.0': - resolution: {integrity: sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==} + '@fortawesome/fontawesome-common-types@7.3.1': + resolution: {integrity: sha512-k0C0sdHmZtAo6dRDtd1Z/qcpyHbL0CKsjV8seMY/21xGhY5Wsv0XRmiI/xEEH4y2c9b1+jvgNs/3EqhV27yUEA==} engines: {node: '>=6'} - '@fortawesome/free-regular-svg-icons@7.2.0': - resolution: {integrity: sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw==} + '@fortawesome/free-regular-svg-icons@7.3.1': + resolution: {integrity: sha512-q1EsmL7Q8DDnkRBUjSvrxbq7c9oVwwVjCn/xa5apKmdp65YSgzUg2y0Ltnd5aDbT6GdAQQdXql5Ha90ArqIReQ==} engines: {node: '>=6'} - '@fortawesome/free-solid-svg-icons@7.2.0': - resolution: {integrity: sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==} + '@fortawesome/free-solid-svg-icons@7.3.1': + resolution: {integrity: sha512-v0BLa0eqg7ubvVWeNSHVBs8fWH/GJicERZoJaxJ3FE/lj67VSqzoMg9pzfZVOfLMX10y0pGwQAuxoRVVH2patg==} engines: {node: '>=6'} '@golevelup/nestjs-discovery@5.0.0': @@ -3517,8 +3517,8 @@ packages: '@maplibre/vt-pbf@4.3.2': resolution: {integrity: sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==} - '@marijn/find-cluster-break@1.0.2': - resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} '@mdi/js@7.4.47': resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} @@ -3684,10 +3684,10 @@ packages: prettier: optional: true - '@nestjs/swagger@11.4.5': - resolution: {integrity: sha512-lvndlJmWBVDOUT0uEtLi6sSpW1syK2/nbAlHBhiELBORMpJGe9+EiWAT9qHtB10jW91L2Jmlwkr0/lttsYZrig==} + '@nestjs/swagger@11.4.6': + resolution: {integrity: sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==} peerDependencies: - '@fastify/static': ^8.0.0 || ^9.0.0 + '@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 class-transformer: '*' @@ -3754,94 +3754,94 @@ packages: '@oazapfts/runtime@1.2.0': resolution: {integrity: sha512-fi7dp7dNayyh/vzqhf0ZdoPfC7tJvYfjaE8MBL1yR+iIsH7cFoqHt+DV70VU49OMCqLc7wQa+yVJcSmIRnV4wA==} - '@opentelemetry/api-logs@0.220.0': - resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + '@opentelemetry/api-logs@0.221.0': + resolution: {integrity: sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.220.0': - resolution: {integrity: sha512-glfIVKnZevRin8fY/9uES/mhRtMT1lGINLHc9MIo5fTQZXswEEHamJtgjv4MTtzgnhHGC92mIS/0lzAUZMyE0w==} + '@opentelemetry/configuration@0.221.0': + resolution: {integrity: sha512-uE9y56Zdi9Gt/RdxYnVOo3YmFZkKJJMA0gqtBe8wh8gdtF5Asqe+Oh/TWiDtFb1s+31jNY4CWgnfIB1KOITfFA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks@2.9.0': - resolution: {integrity: sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==} + '@opentelemetry/context-async-hooks@2.10.0': + resolution: {integrity: sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.9.0': - resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + '@opentelemetry/core@2.10.0': + resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.220.0': - resolution: {integrity: sha512-s0sRPCSlXYqlgObOpCftomJllp3LfUL9FobQ5csg2172ydVhSEnu1ptpsVBJadazs5nUNp7vDuLE03FAFWTLOQ==} + '@opentelemetry/exporter-logs-otlp-grpc@0.221.0': + resolution: {integrity: sha512-txG1G0IrYSsKKMeiWZfj/i5cQmWB+h+hf3HzPpF3RqZVwp+iQQEIsv8Vtmzy6RWVdHdJZfygmVrBI39YTBvWcw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.220.0': - resolution: {integrity: sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==} + '@opentelemetry/exporter-logs-otlp-http@0.221.0': + resolution: {integrity: sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.220.0': - resolution: {integrity: sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==} + '@opentelemetry/exporter-logs-otlp-proto@0.221.0': + resolution: {integrity: sha512-AH6EY+47gXFaWYgG3hfeOneGiE9xIZGtDBk+9g0sM8NZWzsQhhmqPbQQXJzS7pyCh5jRRr2nYNXVrkCmoojRvQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.220.0': - resolution: {integrity: sha512-U128izvJfX/dW9jRGP0gIfadR1Hg7ft3UEGIeRxLFK70m2BWw6AtNCOnsUygpw2zCgR/ygdWbGpcL6TmhW0ZGw==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.221.0': + resolution: {integrity: sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.220.0': - resolution: {integrity: sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==} + '@opentelemetry/exporter-metrics-otlp-http@0.221.0': + resolution: {integrity: sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.220.0': - resolution: {integrity: sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==} + '@opentelemetry/exporter-metrics-otlp-proto@0.221.0': + resolution: {integrity: sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.220.0': - resolution: {integrity: sha512-JZD5DL/NBpVd2BHefvYosm3G40UZ/KzExLv5tc0eZe0CtrsHHtcOk3YPUxR2EINmUeBf8+w5UReTV8fFPn95lA==} + '@opentelemetry/exporter-prometheus@0.221.0': + resolution: {integrity: sha512-kW79a20qWESIuAdDrxzg9WKM98twV/NBWBFRAH57ap/+ssZhiCo0hckzKT0zpuwR/gSHrFAQhJL0bYDrnEM34g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.220.0': - resolution: {integrity: sha512-bv1xmNhmNwIM6MdUBw4yYuJeVcEViVLk3uD69vOQMwueHBnfyl/u0HnBlB1FNY/Te0UOzJzvcbyR8wN6b+iGbA==} + '@opentelemetry/exporter-trace-otlp-grpc@0.221.0': + resolution: {integrity: sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.220.0': - resolution: {integrity: sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==} + '@opentelemetry/exporter-trace-otlp-http@0.221.0': + resolution: {integrity: sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.220.0': - resolution: {integrity: sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==} + '@opentelemetry/exporter-trace-otlp-proto@0.221.0': + resolution: {integrity: sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.9.0': - resolution: {integrity: sha512-RwINoce2BH8T4obT5pMcAla2sWma1YZvYuaktWmTluQ0PkQdvv5D060rWI1+kawX+J2qBRcMbwrZJJNcMJUauQ==} + '@opentelemetry/exporter-zipkin@2.10.0': + resolution: {integrity: sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3852,62 +3852,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.220.0': - resolution: {integrity: sha512-Szt4dO2Boz2CDr38DaSw/lnqwhwKl+IAdgNGEGgSm2Anb+fwPtIAGmIwkhsLLN69QQZQE96JxjMKYY4rlRkYKw==} + '@opentelemetry/instrumentation-http@0.221.0': + resolution: {integrity: sha512-oIP91CPIANuYr09tGFElPFKAh6JUar+awJf1kBRYlaeo9b0gDwZHEB2zBfFlvdNFHm0wAVutMZODVi5smKT30g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.68.0': - resolution: {integrity: sha512-M2MWPoKiMlNWzmW7+AwEwiFpTJQ7bhKpZuo9L3MS/z/KFm2yXY6B43IprAVyiszLFg8/JsF39t8b8wkGpxip2g==} + '@opentelemetry/instrumentation-ioredis@0.69.0': + resolution: {integrity: sha512-I9sZtxXWZ1tRXtRNTEVxpokGtXy6RL1SZhtPVh7zxH78t8ar71V5Dx4bnQiUjKTDzItpC73krD8c0/cEWA9oLg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.66.0': - resolution: {integrity: sha512-ZCzcTWXwlmQsLWGARbUz5fCLpYABoo5A/3PuV5+iICV3pmKWT0rRdKDevkRo0prbzJVh9oEyuT1idxI8ipDqXg==} + '@opentelemetry/instrumentation-nestjs-core@0.67.0': + resolution: {integrity: sha512-lXb7pjobd2i/9Gmihf9wrOM0MgnDYBxOJq7uWEhZOqULodNFLyPCRUnWxSIbPQdUzlU36QtHjuAeaEBUJnqXkA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.72.0': - resolution: {integrity: sha512-p9xrFc/6R8t6Y293sTYLZ83LnzZo/qY0bBPA4xabdQt0Qjt8i1SlYFsIeGY2Jmf5WcESNUdjQB3NxWnt5Ox7zw==} + '@opentelemetry/instrumentation-pg@0.73.0': + resolution: {integrity: sha512-yf3tBVwLHB9cZNNPSToNrthx36ouPe4FctFxy7ya6vSJ6gaiKjNfA/IgFFeuBpZflEQhy6aesPqzZo8ZjFkvNg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.220.0': - resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + '@opentelemetry/instrumentation@0.221.0': + resolution: {integrity: sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.220.0': - resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} + '@opentelemetry/otlp-exporter-base@0.221.0': + resolution: {integrity: sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.220.0': - resolution: {integrity: sha512-/eIkBPMBTIvM3x/0mDX4aJeSkYifYClnBPr68PL1h5LV4VQv4+SV6CGrpiZ4fIWDnobVmhTWCm1J/QRdAWUfvA==} + '@opentelemetry/otlp-grpc-exporter-base@0.221.0': + resolution: {integrity: sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.220.0': - resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} + '@opentelemetry/otlp-transformer@0.221.0': + resolution: {integrity: sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.9.0': - resolution: {integrity: sha512-WrOT1WsOUG+B7hstD2RYoMPIOK76G8E9AQHhMjUvrQaGx/oA7rPWQvvr1Rqv7+yy4R0ZMVwWLC4vW2xnkgWPAQ==} + '@opentelemetry/propagator-b3@2.10.0': + resolution: {integrity: sha512-GnA5B24H+1w8BO21J0q+IWNB0z1v+AGbcquTdIt/dufibhnhgxaA8YKvz0I3akRZhB1jHT+/tlzK+qlAjEDybQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.9.0': - resolution: {integrity: sha512-4mYGty27rYvSM0jtp1ZUOqd3LfVRCYg9H5G9OFzSx5HViYToU21MFhWfco7x1HwXr7ER8yGOiCIHZUwjPksc0Q==} + '@opentelemetry/propagator-jaeger@2.10.0': + resolution: {integrity: sha512-yw/IX8DL470dSMZJoE82ScfYGp7JWZ/G8kFJo35ZILUVTB2jFPTOaioN+8s09pH0RHsWNhweVZb+ZnjJJpCChg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3916,44 +3916,44 @@ packages: resolution: {integrity: sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resources@2.9.0': - resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + '@opentelemetry/resources@2.10.0': + resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.220.0': - resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + '@opentelemetry/sdk-logs@0.221.0': + resolution: {integrity: sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.9.0': - resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + '@opentelemetry/sdk-metrics@2.10.0': + resolution: {integrity: sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.220.0': - resolution: {integrity: sha512-wHtGyHhSKHNH3fym33xRu4Ef/HXTFvX8eQ42xdQdEO9LYx9Y2qNyBDJytyqVlvmo6abWZlNYTUthuAGUMYqYnQ==} + '@opentelemetry/sdk-node@0.221.0': + resolution: {integrity: sha512-UbYuvtBrQQB5Prsh9KOKy4kxzexFxfMs5MkteHeWMoswsEB7kiNhyUVkAOFW/qsEzNHtrkgyghrD2ilZJa+5YA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.9.0': - resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} + '@opentelemetry/sdk-trace-base@2.10.0': + resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.9.0': - resolution: {integrity: sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==} + '@opentelemetry/sdk-trace-node@2.10.0': + resolution: {integrity: sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace@2.9.0': - resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + '@opentelemetry/sdk-trace@2.10.0': + resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -4099,35 +4099,35 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} - '@photo-sphere-viewer/core@5.14.3': - resolution: {integrity: sha512-gSDzM1fuVfWQiDf8Y4AgI+un/wsKmeFO7cY4IFEbjhD1BTXUviSL8GuCXABNfPK2EThZLJqpOXxrbYAhblJrfQ==} + '@photo-sphere-viewer/core@5.15.0': + resolution: {integrity: sha512-Mf72l3R8iBf+Im5duv7IVkX3uiKU1BT6bzzXmdE43KahQgNftt9pYKqTzN1YjaYLAzWDBCFZj/bEK1dIZXk9Gg==} - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.3': - resolution: {integrity: sha512-jgdFMAGkVgtybq0d+zP3Qbu8SNw1Makw/EOpqyeo1klHK+S5JBRp1ibtq/9NRPtYTlf3Ral03iYR6iDgJuxJbg==} + '@photo-sphere-viewer/equirectangular-video-adapter@5.15.0': + resolution: {integrity: sha512-Gro1wUA5qTla71uSeGlQFr9BenNXrI9Xee4a+dveafqrZJ35/n+nDhxrc8I4osykyvC6Foc0qgksAsaNsMKX/w==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/video-plugin': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/video-plugin': 5.15.0 - '@photo-sphere-viewer/markers-plugin@5.14.3': - resolution: {integrity: sha512-OAXHfXR2riaBwS8+OhqcAT2KZ3ch1QI9mJaBq6R9Abs+hcZ5f6QbWJ0IS9SFIGcdHFgBbz6+ea1FcwA9V3zx4g==} + '@photo-sphere-viewer/markers-plugin@5.15.0': + resolution: {integrity: sha512-r1Kv4BkvcOf+77QU7k0pHzZ3O0IrX+ff7arttmb0D81HQHRl/fUcE+tXu+mGDj9B1zuSi1LcvKOsUI9m96owNw==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/resolution-plugin@5.14.3': - resolution: {integrity: sha512-3W4DMhWcYjZdc65NjjLk49Nr7FTczCeRu6DVApfaN/OXfKW6IEFlWKJyrLpXUdQaNB8LPk6GuD0MFNGp8SHAwQ==} + '@photo-sphere-viewer/resolution-plugin@5.15.0': + resolution: {integrity: sha512-8Rm9r9WJX5m8N0Q7ezUEHUgxWybroj6HhfaksINUm7o8WegqZ+Pp2izEEoXI57kkjfudYOp7abrwA/J3I+QRrA==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/settings-plugin': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/settings-plugin': 5.15.0 - '@photo-sphere-viewer/settings-plugin@5.14.3': - resolution: {integrity: sha512-EMaBRXIBIfP+jTOQSvkN00Bhh+ia+G8Bcxak0ydoGb5yaDg9w86Q7/NdK0NNpwXFqcQIAaoDzyX6UEi8Udic5g==} + '@photo-sphere-viewer/settings-plugin@5.15.0': + resolution: {integrity: sha512-xZot+fT5R+Jy7D/QEVoylIMg/R+l/iSBykQ1v8wqGkSrUqJdVkNv3w3ksycb9BYFt/78P8HU7AGvJgIF6J3EFQ==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/video-plugin@5.14.3': - resolution: {integrity: sha512-yMUd3JG0ROJpJkPXJbdFiIkz+m6lDMRrIeLm/bwXvaX+Q1uSgyX0ipIkJrQnAtdk/IU5fdyqBwfn9ExlQ27c2A==} + '@photo-sphere-viewer/video-plugin@5.15.0': + resolution: {integrity: sha512-RG70vRu+suV7QT1WbnxVknm8KVU26wCZx4NUofFAENYSvyldlRxmVK3XqCh1WChWhcjrXqj3woz4vnzrykVOzQ==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 '@photostructure/tz-lookup@11.5.0': resolution: {integrity: sha512-0DVFriinZ7TeOnm9ytXeSL3NMFU87ZqMjgbPNkd8LgHFLcPg1BDyM1eewFYs+pPM+62S4fSP9Mtgijmn+6y95w==} @@ -4140,9 +4140,9 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@playwright/test@1.61.1': - resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} - engines: {node: '>=18'} + '@playwright/test@1.62.0': + resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} + engines: {node: '>=20'} hasBin: true '@pnpm/config.env-replace@1.1.0': @@ -4692,8 +4692,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.69.3': - resolution: {integrity: sha512-cphwqMRcE19/9VkrIPr5qZhQ0SptSSDfDzRUpYHu9OJDFGuYBFyJzK+KQA27wB4YG32O/yF2QjBkDmTyo0vtCw==} + '@sveltejs/kit@2.70.1': + resolution: {integrity: sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -4797,86 +4797,86 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.15.43': - resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} + '@swc/core-darwin-arm64@1.15.46': + resolution: {integrity: sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.43': - resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} + '@swc/core-darwin-x64@1.15.46': + resolution: {integrity: sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.43': - resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} + '@swc/core-linux-arm-gnueabihf@1.15.46': + resolution: {integrity: sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.43': - resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} + '@swc/core-linux-arm64-gnu@1.15.46': + resolution: {integrity: sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.43': - resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} + '@swc/core-linux-arm64-musl@1.15.46': + resolution: {integrity: sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [musl] - '@swc/core-linux-ppc64-gnu@1.15.43': - resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + '@swc/core-linux-ppc64-gnu@1.15.46': + resolution: {integrity: sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] libc: [glibc] - '@swc/core-linux-s390x-gnu@1.15.43': - resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + '@swc/core-linux-s390x-gnu@1.15.46': + resolution: {integrity: sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==} engines: {node: '>=10'} cpu: [s390x] os: [linux] libc: [glibc] - '@swc/core-linux-x64-gnu@1.15.43': - resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} + '@swc/core-linux-x64-gnu@1.15.46': + resolution: {integrity: sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [glibc] - '@swc/core-linux-x64-musl@1.15.43': - resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} + '@swc/core-linux-x64-musl@1.15.46': + resolution: {integrity: sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.43': - resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} + '@swc/core-win32-arm64-msvc@1.15.46': + resolution: {integrity: sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.43': - resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} + '@swc/core-win32-ia32-msvc@1.15.46': + resolution: {integrity: sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.43': - resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} + '@swc/core-win32-x64-msvc@1.15.46': + resolution: {integrity: sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.43': - resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} + '@swc/core@1.15.46': + resolution: {integrity: sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4897,69 +4897,69 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@tailwindcss/node@4.3.2': - resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - '@tailwindcss/oxide-android-arm64@4.3.2': - resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.2': - resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.2': - resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.2': - resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': - resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': - resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.2': - resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.2': - resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.2': - resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.2': - resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4970,24 +4970,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': - resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.2': - resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.2': - resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.3.2': - resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==} + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 @@ -4995,9 +4995,11 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@7.0.0': + resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' '@testing-library/svelte-core@1.1.3': resolution: {integrity: sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==} @@ -5505,63 +5507,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.64.0': - resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.64.0 + '@typescript-eslint/parser': ^8.65.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.64.0': - resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} 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.64.0': - resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.64.0': - resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.64.0': - resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.64.0': - resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} 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.64.0': - resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.64.0': - resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.64.0': - resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} 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.64.0': - resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/typescript-aix-ppc64@7.0.2': @@ -5864,8 +5866,8 @@ packages: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -6240,9 +6242,9 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -6281,8 +6283,8 @@ packages: resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} engines: {node: '>=18.20'} - bullmq@5.80.5: - resolution: {integrity: sha512-3cVpkFXvmi7U7clGQGqlIzWLNJgX77Q0U0xCfbsiu6CFdiAcrKem1rK+xHIq0jIzMA6swq9ss6uvMqzCeTbAgg==} + bullmq@5.81.2: + resolution: {integrity: sha512-Hi9GaVCC6HE9bQP65j/FNv1aL1fcEukTF99ezS5pl1Ud+joCFpNWiPCV45mWdkEmpuacS0XJdMMFGKPIHCwoPg==} engines: {node: '>=12.22.0'} peerDependencies: redis: '>=5.0.0' @@ -6799,12 +6801,13 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cron@4.4.0: resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} @@ -7265,8 +7268,8 @@ packages: engines: {node: '>= 16.0.0'} hasBin: true - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devalue@5.8.2: + resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -7437,12 +7440,8 @@ packages: resolution: {integrity: sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.21.6: - resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} - engines: {node: '>=10.13.0'} - - enhanced-resolve@5.24.2: - resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} + enhanced-resolve@5.24.4: + resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==} engines: {node: '>=10.13.0'} entities@2.2.0: @@ -7560,8 +7559,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-better-tailwindcss@4.6.1: - resolution: {integrity: sha512-Lr8mPyuaZ+dS6ATuJaPwcOFOpOUsRBs5TXyOPqiTzLy0SvK4+0G6usbklCuQn4QabwFtNKdSXwl2WxOIPvu0lQ==} + eslint-plugin-better-tailwindcss@4.7.0: + resolution: {integrity: sha512-lrdlVW4pzLPj/zX5HRqMhKPesGijDfRhnSRJMlNcsrCyJHsndmHCLKTiRNa1eREUZ6G3D3QjdLc+G4tlfGrmkw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -7593,8 +7592,8 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-svelte@3.20.0: - resolution: {integrity: sha512-AElKLVt7Hjy4d7ljwhrhw9hux60DCxCNkmK8cY/aAXvjs8tpR7PvU4DlyI/SA1PaJww1gh0wPGo2pbyURuEwxQ==} + eslint-plugin-svelte@3.22.0: + resolution: {integrity: sha512-O3qn0NePTWta+1o25dIThqeEP/hEQ3VxDK2LVO8SQ5wG9umLMvulK+m1yQ4JGOb2Pkl8IB0G1lpRV/HXDXSLTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 @@ -7633,8 +7632,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.7.0: - resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} + eslint@10.8.0: + resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -7827,8 +7826,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.3: - resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -7917,8 +7916,8 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} fluent-ffmpeg@2.1.3: resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} @@ -8180,8 +8179,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.10.6: - resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} + happy-dom@20.11.1: + resolution: {integrity: sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -8448,8 +8447,8 @@ packages: immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} - immutable-json-patch@6.0.2: - resolution: {integrity: sha512-KwCA5DXJiyldda8SPha1zB+6+vbEi5/jRRcYii/6yFXlyu9ZjiSH/wPq8Ri2Hk8iGjjTMcHW3Z21S4MOpl7sOw==} + immutable-json-patch@6.0.3: + resolution: {integrity: sha512-qFLRzQLjteiTGy5tRJcYqvuu0r/GdQ1fE27IyNBrJ/k7yGSO0xFr6QX62TtmIqSvjQF3afcNFUHsGPYW7ydaHQ==} immutable@5.1.6: resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} @@ -8780,8 +8779,8 @@ packages: joi@17.13.4: resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jose@6.2.4: + resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -8796,6 +8795,14 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + js-yaml@5.2.1: + resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} + hasBin: true + + js-yaml@5.2.2: + resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -8854,8 +8861,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - jsonrepair@3.14.0: - resolution: {integrity: sha512-tWPGKMZf/8UPim+fcW2EfcQ/d/7aKUrP6IECz9G3Tu6Q5dX0orSleqJ9z6sSw7qrQkjF8/Edo4DvsWBZ8H+HNg==} + jsonrepair@3.15.0: + resolution: {integrity: sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==} hasBin: true jsonwebtoken@9.0.3: @@ -9352,8 +9359,8 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - mdn-data@2.28.1: - resolution: {integrity: sha512-U9w+PzSZ00Z5m9rZ5ARVFL5xOfuCHdKYi/1RRwDCJsboFgJDNT3zT6PIPD7mZQYaQLhsZM3GfDRgSMRHhSmVng==} + mdn-data@2.29.0: + resolution: {integrity: sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==} media-chrome@4.19.2: resolution: {integrity: sha512-4ai1ITN8wBhwugQcRgqe3tN0z6OSKGOXqHLNrS04MgKFfsLqu6Dm8MPq02pI9Y9ZKoXtFjIl85jOryIW9es3BA==} @@ -9596,8 +9603,8 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -9761,9 +9768,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.16: - resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} - engines: {node: ^18 || >=20} + nanoid@6.0.0: + resolution: {integrity: sha512-mkUH+rPkwU2qPadJ0oJZOjeZ5Mxn8Q1UhevwkTRWNuUZzyia3h4rhzK39hxaHTk0o2OxB8W2SQ6A8k23ZDi1pQ==} + engines: {node: ^22 || ^24 || >=26} hasBin: true napi-build-utils@2.0.0: @@ -9826,8 +9833,8 @@ packages: '@nestjs/core': '>= 11 < 12' rxjs: ^7.1.0 - nestjs-zod@5.4.0: - resolution: {integrity: sha512-dxVpy1fjfK4kp+ztK+7xQP46fpvZxkeR/jcEdIvEGh/2o71iwXuy/hrKOWSPhJ1nQXV4iBdHqMizndn2GTaXDg==} + nestjs-zod@5.5.0: + resolution: {integrity: sha512-V0WMtmICygYE2RoMKk8ExEeoetkC7PoUmUlZNP4JNk1YgrnUnG1vgkRn2DBNqsDi+Obv3c9AOssmXe/k/AvBOg==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 @@ -9986,8 +9993,8 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} - oidc-provider@9.9.1: - resolution: {integrity: sha512-2kvtykfLu3FJSzaIStg8Dz58/pLtAwI3j/vLdy7KXJBDuJJAEgZhOSWklc+MTjKBLlQjeFwBXpKoE0Pf23pnIQ==} + oidc-provider@9.10.0: + resolution: {integrity: sha512-Olmg6oxgHIviZnrf9yaey6vdaWG1f19UiyvpSj0gCgj6wl9YMcOw3uDv3FHj+x62EXeFj7GfZeTL09MCgIEKnw==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -10290,14 +10297,14 @@ packages: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} - playwright-core@1.61.1: - resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} - engines: {node: '>=18'} + playwright-core@1.62.0: + resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + engines: {node: '>=20'} hasBin: true - playwright@1.61.1: - resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} - engines: {node: '>=18'} + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} hasBin: true plimit-lit@1.6.1: @@ -10778,8 +10785,8 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss@8.5.19: - resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -10846,8 +10853,8 @@ packages: prettier: ^3.0.0 svelte: ^5.0.0 - prettier@3.9.5: - resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} hasBin: true @@ -10998,6 +11005,10 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + raw-body@4.0.0: + resolution: {integrity: sha512-TMHtwexrgOt9VJ2E5JF9RO8mRXGNgC5wXvKkc5AVSJUt1L5gxvjg7eiCQl96EqUEpCWrnNEkCnbAplYtyuq1IA==} + engines: {node: '>=22'} + raw-loader@4.0.2: resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} engines: {node: '>= 10.13.0'} @@ -11008,10 +11019,10 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: - react: ^19.2.7 + react: ^19.2.8 react-email@5.2.11: resolution: {integrity: sha512-9TzTGRGeavli/iv1RICVOePnFOeG2YRyr8kAyXj6Zgudteq60uA0txDwe4q1zIjQ+08fcbzBkSnI9HgPkSK5OA==} @@ -11056,8 +11067,8 @@ packages: peerDependencies: react: '>=15' - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -11354,8 +11365,8 @@ packages: sanitize-filename@1.6.4: resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} - sass@1.101.0: - resolution: {integrity: sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==} + sass@1.102.0: + resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==} engines: {node: '>=20.19.0'} hasBin: true @@ -11509,8 +11520,8 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-icons@16.26.0: - resolution: {integrity: sha512-T9rNJtyOshULM8heLlvrZY346g9zOgZILQ3vxP+FWcX13RhaOLez7YM/hNCEx3b5gJckSCNDoNsjZuDncSMYSQ==} + simple-icons@16.27.1: + resolution: {integrity: sha512-slZF8iKxkv7Lb9SF3L1AcIl5iYLNvDrKKm8yV69cG66XvxJ12SX+bYTgxRwCY4bXSmklGXoXaKEyVmEypOCeqw==} engines: {node: '>=0.12.18'} sirv@2.0.4: @@ -11816,8 +11827,8 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5 - svelte-jsoneditor@3.12.0: - resolution: {integrity: sha512-BUWsAmmDbQTs4AAMvVGA09X1aP8l1kr7dDdzbLVPX8895Ov52PVoAlAsCXERPh+GIan+qZTEBqwSqbM3VRMBaw==} + svelte-jsoneditor@3.13.0: + resolution: {integrity: sha512-4L7jqVulv180NytA12jQfFguUKrkHjQ0KJkrSbxhC7mWZL6tDzcsRKqXNtfCgy9c5CKC340xJmUClVF3VIsONg==} peerDependencies: svelte: ^5.0.0 @@ -11859,8 +11870,8 @@ packages: peerDependencies: svelte: ^5.30.2 - svelte@5.56.5: - resolution: {integrity: sha512-P03YJmUy2JoOxYHb4Ka3oFat1hq2ko2it1MOItSjsJ4B6WgZPLc3+RyyU+57OGWhs3Ieq74EJpZtSnNXdAGgDw==} + svelte@5.56.8: + resolution: {integrity: sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==} engines: {node: '>=18'} svg-parser@2.0.4: @@ -11942,8 +11953,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.3.2: - resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -12045,9 +12056,6 @@ packages: peerDependencies: tslib: ^2 - three@0.184.0: - resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==} - three@0.185.1: resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} @@ -12275,8 +12283,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.64.0: - resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -13269,20 +13277,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -13309,32 +13317,32 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7)': + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7)': + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.12 transitivePeerDependencies: @@ -13342,26 +13350,26 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-member-expression-to-functions@7.29.7': + '@babel/helper-member-expression-to-functions@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -13371,27 +13379,27 @@ snapshots: '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)': + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-wrap-function': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/helper-wrap-function': 7.29.7(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -13402,10 +13410,10 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} - '@babel/helper-wrap-function@7.29.7': + '@babel/helper-wrap-function@7.29.7(supports-color@8.1.1)': dependencies: '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -13423,578 +13431,578 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-globals': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/template': 7.29.7 - '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-constant-elements@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-constant-elements@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-pure-annotations@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/preset-env@7.29.7(@babel/core@7.29.7)': + '@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7) - '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7) - '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7) - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) - babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@8.1.1)) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/types': 7.29.7 esutils: 2.0.3 - '@babel/preset-react@7.29.7(@babel/core@7.29.7)': + '@babel/preset-react@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -14006,19 +14014,19 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.27.0': + '@babel/traverse@7.27.0(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -14026,7 +14034,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -14047,27 +14055,27 @@ snapshots: '@codemirror/autocomplete@6.20.3': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@lezer/common': 1.5.2 - '@codemirror/commands@6.10.3': + '@codemirror/commands@6.10.4': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@lezer/common': 1.5.2 '@codemirror/lang-json@6.0.2': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/json': 1.0.3 - '@codemirror/language@6.12.3': + '@codemirror/language@6.12.4': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -14075,24 +14083,24 @@ snapshots: '@codemirror/lint@6.9.7': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 - crelt: 1.0.6 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + crelt: 1.0.7 - '@codemirror/search@6.7.0': + '@codemirror/search@6.7.1': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 - crelt: 1.0.6 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + crelt: 1.0.7 - '@codemirror/state@6.6.0': + '@codemirror/state@6.7.1': dependencies: - '@marijn/find-cluster-break': 1.0.2 + '@marijn/find-cluster-break': 1.0.3 - '@codemirror/view@6.43.1': + '@codemirror/view@6.43.7': dependencies: - '@codemirror/state': 6.6.0 - crelt: 1.0.6 + '@codemirror/state': 6.7.1 + crelt: 1.0.7 style-mod: 4.1.3 w3c-keyname: 2.2.8 @@ -14129,272 +14137,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.19)': + '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-cascade-layers@5.0.2(postcss@8.5.19)': + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.25)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.19)': + '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-color-function@4.0.12(postcss@8.5.19)': + '@csstools/postcss-color-function@4.0.12(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-color-mix-function@3.0.12(postcss@8.5.19)': + '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.19)': + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-content-alt-text@2.0.8(postcss@8.5.19)': + '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-contrast-color-function@2.0.12(postcss@8.5.19)': + '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-exponential-functions@2.0.9(postcss@8.5.19)': + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.19)': + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.25)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.19)': + '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.19)': + '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-hwb-function@4.0.12(postcss@8.5.19)': + '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-ic-unit@4.0.4(postcss@8.5.19)': + '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.25)': dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@2.0.1(postcss@8.5.19)': + '@csstools/postcss-initial@2.0.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.19)': + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.25)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.19)': + '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-logical-float-and-clear@3.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.19)': + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.25)': dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.19)': + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.19)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.19)': + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.25)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.19)': + '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.19)': + '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-position-area-property@1.0.0(postcss@8.5.19)': + '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.19)': + '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.19)': + '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.25)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-random-function@2.0.1(postcss@8.5.19)': + '@csstools/postcss-random-function@2.0.1(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.19)': + '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.25)': 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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-scope-pseudo-class@4.0.1(postcss@8.5.19)': + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.19)': + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.19)': + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.19)': + '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.25)': dependencies: '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.19)': + '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.25)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.19)': + '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.25)': dependencies: '@csstools/color-helpers': 5.1.0 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.19)': + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.25)': 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.19 + postcss: 8.5.25 - '@csstools/postcss-unset-value@4.0.0(postcss@8.5.19)': + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.4)': dependencies: @@ -14404,47 +14412,47 @@ snapshots: dependencies: postcss-selector-parser: 7.1.4 - '@csstools/utilities@2.0.0(postcss@8.5.19)': + '@csstools/utilities@2.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.6.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docsearch/core@4.6.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': optionalDependencies: '@types/react': 19.2.17 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + 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.7(react@19.2.7))(react@19.2.7)(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)': 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.7(react@19.2.7))(react@19.2.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/css': 4.6.3 optionalDependencies: '@types/react': 19.2.17 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - '@docusaurus/babel@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.7 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7) - '@babel/preset-react': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-react': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/runtime': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.6 tslib: 2.8.1 @@ -14466,66 +14474,32 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/babel@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7) - '@babel/preset-react': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/runtime': 7.29.7 - '@babel/traverse': 7.29.7 - '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - babel-plugin-dynamic-import-node: 2.3.3 - fs-extra: 11.3.6 - tslib: 2.8.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - react - - react-dom - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/bundler@3.10.2(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@babel/core': 7.29.7 - '@docusaurus/babel': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/cssnano-preset': 3.10.2 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - babel-loader: 9.2.1(@babel/core@7.29.7)(webpack@5.108.4(postcss@8.5.19)) + '@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)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.108.4(postcss@8.5.19)) - css-loader: 6.11.0(webpack@5.108.4(postcss@8.5.19)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.108.4(postcss@8.5.19)) - cssnano: 6.1.2(postcss@8.5.19) - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) + 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)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.10.2(webpack@5.108.4(postcss@8.5.19)) - null-loader: 4.0.1(webpack@5.108.4(postcss@8.5.19)) - postcss: 8.5.19 - postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) - postcss-preset-env: 10.6.1(postcss@8.5.19) - terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) + 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)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) - webpack: 5.108.4(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19) - webpackbar: 7.0.0(webpack@5.108.4(postcss@8.5.19)) + 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)) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -14543,16 +14517,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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/babel': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/bundler': 3.10.2(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) + '@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) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -14567,28 +14541,28 @@ 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(postcss@8.5.19)) + 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)) leven: 3.1.0 lodash: 4.18.1 open: 8.4.2 p-map: 4.0.0 prompts: 2.4.2 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' - react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.108.4(postcss@8.5.19)) - react-router: 5.3.4(react@19.2.7) - react-router-config: 5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7) - react-router-dom: 5.3.4(react@19.2.7) + 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)' + 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-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) semver: 7.8.5 serve-handler: 6.1.7 tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.108.4(postcss@8.5.19) + 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-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.6(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)) + 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-merge: 6.0.1 transitivePeerDependencies: - '@minify-html/node' @@ -14614,9 +14588,9 @@ snapshots: '@docusaurus/cssnano-preset@3.10.2': dependencies: - cssnano-preset-advanced: 6.1.2(postcss@8.5.19) - postcss: 8.5.19 - postcss-sort-media-queries: 5.2.0(postcss@8.5.19) + 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) tslib: 2.8.1 '@docusaurus/logger@3.10.2': @@ -14624,34 +14598,34 @@ snapshots: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mdx-js/mdx': 3.1.1 + '@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) + '@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(postcss@8.5.19)) + 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)) fs-extra: 11.3.6 image-size: 2.0.2 - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@8.1.1) mdast-util-to-string: 4.0.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) rehype-raw: 7.0.0 - remark-directive: 3.0.1 + remark-directive: 3.0.1(supports-color@8.1.1) remark-emoji: 4.0.1 - remark-frontmatter: 5.0.0 - remark-gfm: 4.0.1 + remark-frontmatter: 5.0.0(supports-color@8.1.1) + remark-gfm: 4.0.1(supports-color@8.1.1) stringify-object: 3.3.0 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(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) + 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)) vfile: 6.0.3 - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -14668,17 +14642,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) '@types/history': 4.7.11 '@types/react': 19.2.17 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' + 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)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -14695,30 +14669,30 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) cheerio: 1.0.0-rc.12 combine-promises: 1.2.0 feed: 4.2.2 fs-extra: 11.3.6 lodash: 4.18.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) schema-dts: 1.1.5 srcset: 4.0.0 tslib: 2.8.1 unist-util-visit: 5.1.0 utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14743,28 +14717,28 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.6 js-yaml: 4.3.0 lodash: 4.18.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14789,18 +14763,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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) fs-extra: 11.3.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14825,12 +14799,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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14858,15 +14832,15 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) fs-extra: 11.3.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-json-view-lite: 2.5.0(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-json-view-lite: 2.5.0(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14892,13 +14866,13 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@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) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14924,13 +14898,13 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@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) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14956,13 +14930,13 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@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) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14988,17 +14962,17 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/logger': 3.10.2 - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) fs-extra: 11.3.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) sitemap: 7.1.3 tslib: 2.8.1 transitivePeerDependencies: @@ -15025,18 +14999,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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) - '@svgr/webpack': 8.1.0(@typescript/typescript6@6.0.2) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@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) + '@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(postcss@8.5.19) + 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) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -15061,25 +15035,25 @@ 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.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-css-cascade-layers': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-debug': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-google-analytics': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-google-gtag': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-google-tag-manager': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-sitemap': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-svgr': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-classic': 3.10.2(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@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) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) transitivePeerDependencies: - '@algolia/client-search' - '@docusaurus/faster' @@ -15107,38 +15081,38 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/react-loadable@6.0.0(react@19.2.7)': + '@docusaurus/react-loadable@6.0.0(react@19.2.8)': dependencies: '@types/react': 19.2.17 - react: 19.2.7 + react: 19.2.8 - '@docusaurus/theme-classic@3.10.2(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/theme-translations': 3.10.2 - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) + '@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) 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.19 - prism-react-renderer: 2.4.1(react@19.2.7) + postcss: 8.5.25 + prism-react-renderer: 2.4.1(react@19.2.8) prismjs: 1.30.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-router-dom: 5.3.4(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router-dom: 5.3.4(react@19.2.8) rtlcss: 4.3.0 tslib: 2.8.1 utility-types: 3.11.0 @@ -15165,21 +15139,21 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) '@types/history': 4.7.11 '@types/react': 19.2.17 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 - prism-react-renderer: 2.4.1(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + prism-react-renderer: 2.4.1(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 utility-types: 3.11.0 transitivePeerDependencies: @@ -15198,16 +15172,16 @@ 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) mermaid: 11.16.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -15234,25 +15208,25 @@ 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.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.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)': 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.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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/theme-translations': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) algoliasearch: 5.56.0 algoliasearch-helper: 3.29.2(algoliasearch@5.56.0) clsx: 2.1.1 eta: 2.2.0 fs-extra: 11.3.6 lodash: 4.18.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 utility-types: 3.11.0 transitivePeerDependencies: @@ -15289,19 +15263,19 @@ snapshots: '@docusaurus/tsconfig@3.10.2': {} - '@docusaurus/types@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@mdx-js/mdx': 3.1.1 + '@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 commander: 5.1.0 joi: 17.13.4 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' + 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(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19) + 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-merge: 5.10.0 transitivePeerDependencies: - '@minify-html/node' @@ -15319,39 +15293,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/types@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: - '@mdx-js/mdx': 3.1.1 - '@types/history': 4.7.11 - '@types/mdast': 4.0.4 - '@types/react': 19.2.17 - commander: 5.1.0 - joi: 17.13.4 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' - utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) - webpack-merge: 5.10.0 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils-common@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@docusaurus/types': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) tslib: 2.8.1 transitivePeerDependencies: - '@minify-html/node' @@ -15371,33 +15315,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - tslib: 2.8.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - react - - react-dom - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils-validation@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) fs-extra: 11.3.6 joi: 17.13.4 js-yaml: 4.3.0 @@ -15421,15 +15343,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@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)': dependencies: '@11ty/gray-matter': 1.0.0 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) + 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)) fs-extra: 11.3.6 github-slugger: 1.5.0 globby: 11.1.0 @@ -15441,50 +15363,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(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) + 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)) utility-types: 3.11.0 - webpack: 5.108.4(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19) - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - react - - react-dom - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@11ty/gray-matter': 1.0.0 - '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - escape-string-regexp: 4.0.0 - execa: 5.1.1 - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) - fs-extra: 11.3.6 - github-slugger: 1.5.0 - globby: 11.1.0 - jiti: 1.21.7 - js-yaml: 4.3.0 - lodash: 4.18.1 - micromatch: 4.0.8 - p-queue: 6.6.2 - prompts: 2.4.2 - resolve-pathname: 3.0.0 - tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) - utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -15744,22 +15625,22 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))': dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 - minimatch: 10.2.5 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.6 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.6.0': + '@eslint/config-helpers@0.7.0': dependencies: '@eslint/core': 1.2.1 @@ -15767,14 +15648,14 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/css-tree@4.0.4': + '@eslint/css-tree@4.0.5': dependencies: - mdn-data: 2.28.1 + mdn-data: 2.29.0 source-map-js: 1.2.1 - '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))': optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) '@eslint/object-schema@3.0.5': {} @@ -15794,18 +15675,18 @@ snapshots: '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.9.5 + prettier: 3.9.6 - '@floating-ui/core@1.7.5': + '@floating-ui/core@1.8.0': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.6': + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.12': {} '@formatjs/ecma402-abstract@2.3.6': dependencies: @@ -15841,20 +15722,20 @@ snapshots: dependencies: tslib: 2.8.1 - '@fortawesome/fontawesome-common-types@7.2.0': {} + '@fortawesome/fontawesome-common-types@7.3.1': {} - '@fortawesome/free-regular-svg-icons@7.2.0': + '@fortawesome/free-regular-svg-icons@7.3.1': dependencies: - '@fortawesome/fontawesome-common-types': 7.2.0 + '@fortawesome/fontawesome-common-types': 7.3.1 - '@fortawesome/free-solid-svg-icons@7.2.0': + '@fortawesome/free-solid-svg-icons@7.3.1': dependencies: - '@fortawesome/fontawesome-common-types': 7.2.0 + '@fortawesome/fontawesome-common-types': 7.3.1 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@golevelup/nestjs-discovery@5.0.0(@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) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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) lodash: 4.18.1 '@grpc/grpc-js@1.14.4': @@ -16013,19 +15894,19 @@ snapshots: pg-connection-string: 2.14.0 postgres: 3.4.9 - '@immich/ui@0.83.0(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0))': + '@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))': dependencies: '@internationalized/date': 3.12.2 '@mdi/js': 7.4.47 - '@sveltejs/kit': 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.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)) + 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)) luxon: 3.7.2 - simple-icons: 16.26.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + simple-icons: 16.27.1 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) svelte-highlight: 7.11.0 tailwind-merge: 3.6.0 - tailwind-variants: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.2) - tailwindcss: 4.3.2 + tailwind-variants: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + tailwindcss: 4.3.3 '@inquirer/ansi@1.0.2': {} @@ -16367,9 +16248,9 @@ snapshots: dependencies: vary: 1.1.2 - '@koa/router@15.7.0(koa@3.2.1)': + '@koa/router@15.7.0(koa@3.2.1)(supports-color@8.1.1)': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 koa: 3.2.1 koa-compose: 4.1.0 @@ -16377,20 +16258,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@koddsson/eslint-plugin-tscompat@0.2.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@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.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.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) browserslist: 4.28.6 transitivePeerDependencies: - eslint - supports-color - typescript - '@kwsites/file-exists@1.1.1': + '@kwsites/file-exists@1.1.1(supports-color@8.1.1)': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16418,10 +16299,10 @@ snapshots: '@mapbox/mapbox-gl-rtl-text@0.4.0': {} - '@mapbox/node-pre-gyp@1.0.11': + '@mapbox/node-pre-gyp@1.0.11(supports-color@8.1.1)': dependencies: detect-libc: 2.1.2 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@8.1.1) make-dir: 3.1.0 node-fetch: 2.7.0 nopt: 5.0.0 @@ -16472,7 +16353,7 @@ snapshots: '@types/geojson': 7946.0.16 pbf: 5.1.0 - '@marijn/find-cluster-break@1.0.2': {} + '@marijn/find-cluster-break@1.0.3': {} '@mdi/js@7.4.47': {} @@ -16484,26 +16365,26 @@ snapshots: '@mdn/browser-compat-data@6.1.5': {} - '@mdx-js/mdx@3.1.1': + '@mdx-js/mdx@3.1.1(supports-color@8.1.1)': dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdx': 2.0.14 - acorn: 8.17.0 + acorn: 8.18.0 collapse-white-space: 2.1.0 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1) markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.1(acorn@8.17.0) + recma-jsx: 1.0.1(acorn@8.18.0) recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 + rehype-recma: 1.0.0(supports-color@8.1.1) + remark-mdx: 3.1.1(supports-color@8.1.1) + remark-parse: 11.0.0(supports-color@8.1.1) remark-rehype: 11.1.2 source-map: 0.7.6 unified: 11.0.5 @@ -16514,11 +16395,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7)': + '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': dependencies: '@types/mdx': 2.0.14 '@types/react': 19.2.17 - react: 19.2.7 + react: 19.2.8 '@mermaid-js/parser@1.2.0': dependencies: @@ -16553,42 +16434,42 @@ snapshots: '@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))(@nestjs/core@11.1.28)': + '@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) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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))(@nestjs/core@11.1.28)(bullmq@5.80.5)': + '@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))': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.80.5 + '@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) tslib: 2.8.1 - '@nestjs/cli@11.0.24(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@24.13.3)(esbuild@0.28.1)(lightningcss@1.33.0)(prettier@3.9.5)': + '@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)': dependencies: '@angular-devkit/core': 19.2.27(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) '@angular-devkit/schematics-cli': 19.2.27(@types/node@24.13.3)(chokidar@4.0.3) '@inquirer/prompts': 7.10.1(@types/node@24.13.3) - '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.9.5)(typescript@5.9.3) + '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3) ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.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)) 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.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.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-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@swc/core': 1.15.46(@swc/helpers@0.5.23) transitivePeerDependencies: - '@minify-html/node' - '@swc/css' @@ -16605,9 +16486,9 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1)': dependencies: - file-type: 21.3.4 + file-type: 21.3.4(supports-color@8.1.1) iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -16617,9 +16498,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@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)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.4.2 @@ -16628,45 +16509,45 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) - '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 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)(supports-color@8.1.1) + '@nestjs/websockets': 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-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) reflect-metadata: 0.2.2 - '@nestjs/platform-express@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@nestjs/platform-express@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)(supports-color@8.1.1)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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) cors: 2.8.6 - express: 5.2.1 + express: 5.2.1(supports-color@8.1.1) multer: 2.2.0 path-to-regexp: 8.4.2 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@8.1.1)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/websockets': 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-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 - socket.io: 4.8.3 + socket.io: 4.8.3(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@nestjs/schedule@6.1.3(@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) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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) cron: 4.4.0 - '@nestjs/schematics@11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.5)': + '@nestjs/schematics@11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.6)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) @@ -16675,11 +16556,11 @@ snapshots: pluralize: 8.0.0 typescript: '@typescript/typescript6@6.0.2' optionalDependencies: - prettier: 3.9.5 + prettier: 3.9.6 transitivePeerDependencies: - chokidar - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.5)(typescript@5.9.3)': + '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) @@ -16688,42 +16569,42 @@ snapshots: pluralize: 8.0.0 typescript: 5.9.3 optionalDependencies: - prettier: 3.9.5 + prettier: 3.9.6 transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2)': + '@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)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) - js-yaml: 4.3.0 + '@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) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(reflect-metadata@0.2.2) + js-yaml: 5.2.1 lodash: 4.18.1 path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 swagger-ui-dist: 5.32.8 typescript: '@typescript/typescript6@6.0.2' - '@nestjs/testing@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28)': + '@nestjs/testing@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)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + '@nestjs/platform-express': 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)(supports-color@8.1.1) - '@nestjs/websockets@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@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-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@8.1.1) '@noble/hashes@1.4.0': {} @@ -16745,125 +16626,108 @@ snapshots: '@oazapfts/runtime@1.2.0': {} - '@opentelemetry/api-logs@0.220.0': + '@opentelemetry/api-logs@0.221.0': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.1': {} - '@opentelemetry/configuration@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/configuration@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) yaml: 2.9.0 - '@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/context-async-hooks@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-http@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-grpc@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-http@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-prometheus@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-http@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-zipkin@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/host-metrics@0.38.3(@opentelemetry/api@1.9.1)': @@ -16871,38 +16735,38 @@ snapshots: '@opentelemetry/api': 1.9.1 systeminformation: 5.31.17 - '@opentelemetry/instrumentation-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-http@0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': 1.43.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.68.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-ioredis@0.69.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/redis-common': 0.38.3 '@opentelemetry/semantic-conventions': 1.43.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.66.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-nestjs-core@0.67.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': 1.43.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.72.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-pg@0.73.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/sql-common': 0.42.0(@opentelemetry/api@1.9.1) '@types/pg': 8.15.6 @@ -16910,124 +16774,124 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation@0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/api-logs': 0.221.0 import-in-the-middle: 3.0.2 - require-in-the-middle: 8.0.1 + require-in-the-middle: 8.0.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-exporter-base@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-grpc-exporter-base@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-transformer@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.221.0 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-b3@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/propagator-b3@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-jaeger@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/propagator-jaeger@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/redis-common@0.38.3': {} - '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.221.0 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-node@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-node@0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/configuration': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-b3': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-jaeger': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.221.0 + '@opentelemetry/configuration': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-prometheus': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-zipkin': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-node@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/semantic-conventions@1.43.0': {} @@ -17035,7 +16899,7 @@ snapshots: '@opentelemetry/sql-common@0.42.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@oxc-project/types@0.139.0': {} @@ -17198,32 +17062,32 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 - '@photo-sphere-viewer/core@5.14.3': + '@photo-sphere-viewer/core@5.15.0': dependencies: - three: 0.184.0 - - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3))': - dependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/video-plugin': 5.14.3(@photo-sphere-viewer/core@5.14.3) three: 0.185.1 - '@photo-sphere-viewer/markers-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': + '@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))': dependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/video-plugin': 5.15.0(@photo-sphere-viewer/core@5.15.0) + three: 0.185.1 - '@photo-sphere-viewer/resolution-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/settings-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3))': + '@photo-sphere-viewer/markers-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': dependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/settings-plugin': 5.14.3(@photo-sphere-viewer/core@5.14.3) + '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/settings-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': + '@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))': dependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@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/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': + '@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': dependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + + '@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + dependencies: + '@photo-sphere-viewer/core': 5.15.0 three: 0.185.1 '@photostructure/tz-lookup@11.5.0': {} @@ -17233,9 +17097,9 @@ snapshots: '@pkgr/core@0.3.6': {} - '@playwright/test@1.61.1': + '@playwright/test@1.62.0': dependencies: - playwright: 1.61.1 + playwright: 1.62.0 '@pnpm/config.env-replace@1.1.0': {} @@ -17271,144 +17135,144 @@ snapshots: '@protobufjs/utf8@1.1.2': {} - '@react-email/body@0.3.0(react@19.2.7)': + '@react-email/body@0.3.0(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/button@0.2.1(react@19.2.7)': + '@react-email/button@0.2.1(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/code-block@0.2.1(react@19.2.7)': + '@react-email/code-block@0.2.1(react@19.2.8)': dependencies: prismjs: 1.30.0 - react: 19.2.7 + react: 19.2.8 - '@react-email/code-inline@0.0.6(react@19.2.7)': + '@react-email/code-inline@0.0.6(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/column@0.0.14(react@19.2.7)': + '@react-email/column@0.0.14(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/components@1.0.12(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@react-email/components@1.0.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@react-email/body': 0.3.0(react@19.2.7) - '@react-email/button': 0.2.1(react@19.2.7) - '@react-email/code-block': 0.2.1(react@19.2.7) - '@react-email/code-inline': 0.0.6(react@19.2.7) - '@react-email/column': 0.0.14(react@19.2.7) - '@react-email/container': 0.0.16(react@19.2.7) - '@react-email/font': 0.0.10(react@19.2.7) - '@react-email/head': 0.0.13(react@19.2.7) - '@react-email/heading': 0.0.16(react@19.2.7) - '@react-email/hr': 0.0.12(react@19.2.7) - '@react-email/html': 0.0.12(react@19.2.7) - '@react-email/img': 0.0.12(react@19.2.7) - '@react-email/link': 0.0.13(react@19.2.7) - '@react-email/markdown': 0.0.18(react@19.2.7) - '@react-email/preview': 0.0.14(react@19.2.7) - '@react-email/render': 2.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@react-email/row': 0.0.13(react@19.2.7) - '@react-email/section': 0.0.17(react@19.2.7) - '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.7))(@react-email/button@0.2.1(react@19.2.7))(@react-email/code-block@0.2.1(react@19.2.7))(@react-email/code-inline@0.0.6(react@19.2.7))(@react-email/container@0.0.16(react@19.2.7))(@react-email/heading@0.0.16(react@19.2.7))(@react-email/hr@0.0.12(react@19.2.7))(@react-email/img@0.0.12(react@19.2.7))(@react-email/link@0.0.13(react@19.2.7))(@react-email/preview@0.0.14(react@19.2.7))(@react-email/text@0.1.6(react@19.2.7))(react@19.2.7) - '@react-email/text': 0.1.6(react@19.2.7) - react: 19.2.7 + '@react-email/body': 0.3.0(react@19.2.8) + '@react-email/button': 0.2.1(react@19.2.8) + '@react-email/code-block': 0.2.1(react@19.2.8) + '@react-email/code-inline': 0.0.6(react@19.2.8) + '@react-email/column': 0.0.14(react@19.2.8) + '@react-email/container': 0.0.16(react@19.2.8) + '@react-email/font': 0.0.10(react@19.2.8) + '@react-email/head': 0.0.13(react@19.2.8) + '@react-email/heading': 0.0.16(react@19.2.8) + '@react-email/hr': 0.0.12(react@19.2.8) + '@react-email/html': 0.0.12(react@19.2.8) + '@react-email/img': 0.0.12(react@19.2.8) + '@react-email/link': 0.0.13(react@19.2.8) + '@react-email/markdown': 0.0.18(react@19.2.8) + '@react-email/preview': 0.0.14(react@19.2.8) + '@react-email/render': 2.0.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@react-email/row': 0.0.13(react@19.2.8) + '@react-email/section': 0.0.17(react@19.2.8) + '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.8))(@react-email/button@0.2.1(react@19.2.8))(@react-email/code-block@0.2.1(react@19.2.8))(@react-email/code-inline@0.0.6(react@19.2.8))(@react-email/container@0.0.16(react@19.2.8))(@react-email/heading@0.0.16(react@19.2.8))(@react-email/hr@0.0.12(react@19.2.8))(@react-email/img@0.0.12(react@19.2.8))(@react-email/link@0.0.13(react@19.2.8))(@react-email/preview@0.0.14(react@19.2.8))(@react-email/text@0.1.6(react@19.2.8))(react@19.2.8) + '@react-email/text': 0.1.6(react@19.2.8) + react: 19.2.8 transitivePeerDependencies: - react-dom - '@react-email/container@0.0.16(react@19.2.7)': + '@react-email/container@0.0.16(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/font@0.0.10(react@19.2.7)': + '@react-email/font@0.0.10(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/head@0.0.13(react@19.2.7)': + '@react-email/head@0.0.13(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/heading@0.0.16(react@19.2.7)': + '@react-email/heading@0.0.16(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/hr@0.0.12(react@19.2.7)': + '@react-email/hr@0.0.12(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/html@0.0.12(react@19.2.7)': + '@react-email/html@0.0.12(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/img@0.0.12(react@19.2.7)': + '@react-email/img@0.0.12(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/link@0.0.13(react@19.2.7)': + '@react-email/link@0.0.13(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/markdown@0.0.18(react@19.2.7)': + '@react-email/markdown@0.0.18(react@19.2.8)': dependencies: marked: 15.0.12 - react: 19.2.7 + react: 19.2.8 - '@react-email/preview@0.0.14(react@19.2.7)': + '@react-email/preview@0.0.14(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/render@2.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@react-email/render@2.0.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: html-to-text: 9.0.5 - prettier: 3.9.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + prettier: 3.9.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - '@react-email/render@2.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@react-email/render@2.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: entities: 4.5.0 html-to-text: 9.0.5 html5parser: 3.0.0 - prettier: 3.9.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + prettier: 3.9.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - '@react-email/row@0.0.13(react@19.2.7)': + '@react-email/row@0.0.13(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/section@0.0.17(react@19.2.7)': + '@react-email/section@0.0.17(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.7))(@react-email/button@0.2.1(react@19.2.7))(@react-email/code-block@0.2.1(react@19.2.7))(@react-email/code-inline@0.0.6(react@19.2.7))(@react-email/container@0.0.16(react@19.2.7))(@react-email/heading@0.0.16(react@19.2.7))(@react-email/hr@0.0.12(react@19.2.7))(@react-email/img@0.0.12(react@19.2.7))(@react-email/link@0.0.13(react@19.2.7))(@react-email/preview@0.0.14(react@19.2.7))(@react-email/text@0.1.6(react@19.2.7))(react@19.2.7)': + '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.8))(@react-email/button@0.2.1(react@19.2.8))(@react-email/code-block@0.2.1(react@19.2.8))(@react-email/code-inline@0.0.6(react@19.2.8))(@react-email/container@0.0.16(react@19.2.8))(@react-email/heading@0.0.16(react@19.2.8))(@react-email/hr@0.0.12(react@19.2.8))(@react-email/img@0.0.12(react@19.2.8))(@react-email/link@0.0.13(react@19.2.8))(@react-email/preview@0.0.14(react@19.2.8))(@react-email/text@0.1.6(react@19.2.8))(react@19.2.8)': dependencies: - '@react-email/text': 0.1.6(react@19.2.7) - react: 19.2.7 - tailwindcss: 4.3.2 + '@react-email/text': 0.1.6(react@19.2.8) + react: 19.2.8 + tailwindcss: 4.3.3 optionalDependencies: - '@react-email/body': 0.3.0(react@19.2.7) - '@react-email/button': 0.2.1(react@19.2.7) - '@react-email/code-block': 0.2.1(react@19.2.7) - '@react-email/code-inline': 0.0.6(react@19.2.7) - '@react-email/container': 0.0.16(react@19.2.7) - '@react-email/heading': 0.0.16(react@19.2.7) - '@react-email/hr': 0.0.12(react@19.2.7) - '@react-email/img': 0.0.12(react@19.2.7) - '@react-email/link': 0.0.13(react@19.2.7) - '@react-email/preview': 0.0.14(react@19.2.7) + '@react-email/body': 0.3.0(react@19.2.8) + '@react-email/button': 0.2.1(react@19.2.8) + '@react-email/code-block': 0.2.1(react@19.2.8) + '@react-email/code-inline': 0.0.6(react@19.2.8) + '@react-email/container': 0.0.16(react@19.2.8) + '@react-email/heading': 0.0.16(react@19.2.8) + '@react-email/hr': 0.0.12(react@19.2.8) + '@react-email/img': 0.0.12(react@19.2.8) + '@react-email/link': 0.0.13(react@19.2.8) + '@react-email/preview': 0.0.14(react@19.2.8) - '@react-email/text@0.1.6(react@19.2.7)': + '@react-email/text@0.1.6(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)': + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7)': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@rolldown/binding-android-arm64@1.1.5': optional: true @@ -17565,13 +17429,13 @@ snapshots: '@sindresorhus/is@5.6.0': {} - '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 invariant: 2.2.4 prop-types: 15.8.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) react-fast-compare: 3.2.2 shallowequal: 1.1.0 @@ -17583,11 +17447,11 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.7)': + '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) notepack.io: 3.0.1 - socket.io-adapter: 2.5.7 + socket.io-adapter: 2.5.7(supports-color@8.1.1) uid2: 1.0.0 transitivePeerDependencies: - supports-color @@ -17596,107 +17460,107 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.11(acorn@8.17.0)': + '@sveltejs/acorn-typescript@1.0.11(acorn@8.18.0)': dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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)))': dependencies: - '@sveltejs/kit': 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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)) - '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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))': dependencies: - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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)) magic-string: 0.30.21 sharp: 0.34.5 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - svelte-parse-markup: 0.1.5(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + 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) vite-imagetools: 9.0.3(rollup@4.62.0) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - '@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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)) '@types/cookie': 0.6.0 - acorn: 8.17.0 + acorn: 8.18.0 cookie: 0.6.0 - devalue: 5.8.1 + devalue: 5.8.2 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.5(@typescript-eslint/types@8.64.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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) optionalDependencies: '@opentelemetry/api': 1.9.1 typescript: '@typescript/typescript6@6.0.2' '@sveltejs/load-config@0.2.0': {} - '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.3 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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) + 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)) - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-preset@8.1.0(@babel/core@7.29.7)': + '@svgr/babel-preset@8.1.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) - '@svgr/core@8.1.0(@typescript/typescript6@6.0.2)': + '@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) camelcase: 6.3.0 cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) snake-case: 3.0.4 @@ -17709,92 +17573,92 @@ snapshots: '@babel/types': 7.29.7 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2))(@typescript/typescript6@6.0.2)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(@typescript/typescript6@6.0.2)': dependencies: - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) + '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) deepmerge: 4.3.1 svgo: 3.3.4 transitivePeerDependencies: - typescript - '@svgr/webpack@8.1.0(@typescript/typescript6@6.0.2)': + '@svgr/webpack@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-constant-elements': 7.29.7(@babel/core@7.29.7) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7) - '@babel/preset-react': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2))(@typescript/typescript6@6.0.2) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-react-constant-elements': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-react': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(supports-color@8.1.1) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(@typescript/typescript6@6.0.2) transitivePeerDependencies: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.43': + '@swc/core-darwin-arm64@1.15.46': optional: true - '@swc/core-darwin-x64@1.15.43': + '@swc/core-darwin-x64@1.15.46': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.43': + '@swc/core-linux-arm-gnueabihf@1.15.46': optional: true - '@swc/core-linux-arm64-gnu@1.15.43': + '@swc/core-linux-arm64-gnu@1.15.46': optional: true - '@swc/core-linux-arm64-musl@1.15.43': + '@swc/core-linux-arm64-musl@1.15.46': optional: true - '@swc/core-linux-ppc64-gnu@1.15.43': + '@swc/core-linux-ppc64-gnu@1.15.46': optional: true - '@swc/core-linux-s390x-gnu@1.15.43': + '@swc/core-linux-s390x-gnu@1.15.46': optional: true - '@swc/core-linux-x64-gnu@1.15.43': + '@swc/core-linux-x64-gnu@1.15.46': optional: true - '@swc/core-linux-x64-musl@1.15.43': + '@swc/core-linux-x64-musl@1.15.46': optional: true - '@swc/core-win32-arm64-msvc@1.15.43': + '@swc/core-win32-arm64-msvc@1.15.46': optional: true - '@swc/core-win32-ia32-msvc@1.15.43': + '@swc/core-win32-ia32-msvc@1.15.46': optional: true - '@swc/core-win32-x64-msvc@1.15.43': + '@swc/core-win32-x64-msvc@1.15.46': optional: true - '@swc/core@1.15.43(@swc/helpers@0.5.23)': + '@swc/core@1.15.46(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.27 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.43 - '@swc/core-darwin-x64': 1.15.43 - '@swc/core-linux-arm-gnueabihf': 1.15.43 - '@swc/core-linux-arm64-gnu': 1.15.43 - '@swc/core-linux-arm64-musl': 1.15.43 - '@swc/core-linux-ppc64-gnu': 1.15.43 - '@swc/core-linux-s390x-gnu': 1.15.43 - '@swc/core-linux-x64-gnu': 1.15.43 - '@swc/core-linux-x64-musl': 1.15.43 - '@swc/core-win32-arm64-msvc': 1.15.43 - '@swc/core-win32-ia32-msvc': 1.15.43 - '@swc/core-win32-x64-msvc': 1.15.43 + '@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/helpers': 0.5.23 '@swc/counter@0.1.3': {} @@ -17811,73 +17675,73 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/node@4.3.2': + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.21.6 + enhanced-resolve: 5.24.4 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.2 + tailwindcss: 4.3.3 - '@tailwindcss/oxide-android-arm64@4.3.2': + '@tailwindcss/oxide-android-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.2': + '@tailwindcss/oxide-darwin-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.2': + '@tailwindcss/oxide-darwin-x64@4.3.3': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.2': + '@tailwindcss/oxide-freebsd-x64@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.2': + '@tailwindcss/oxide-linux-x64-musl@4.3.3': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.2': + '@tailwindcss/oxide-wasm32-wasi@4.3.3': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': optional: true - '@tailwindcss/oxide@4.3.2': + '@tailwindcss/oxide@4.3.3': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.2 - '@tailwindcss/oxide-darwin-arm64': 4.3.2 - '@tailwindcss/oxide-darwin-x64': 4.3.2 - '@tailwindcss/oxide-freebsd-x64': 4.3.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 - '@tailwindcss/oxide-linux-x64-musl': 4.3.2 - '@tailwindcss/oxide-wasm32-wasi': 4.3.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.2(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@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))': dependencies: - '@tailwindcss/node': 4.3.2 - '@tailwindcss/oxide': 4.3.2 - tailwindcss: 4.3.2 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + '@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) '@testing-library/dom@10.4.1': dependencies: @@ -17890,55 +17754,56 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': + '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': dependencies: '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 aria-query: 5.3.1 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte-core@1.1.3(svelte@5.56.5(@typescript-eslint/types@8.64.0))': + '@testing-library/svelte-core@1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0))': dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - '@testing-library/svelte@5.4.2(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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)': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.1.3(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + '@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) optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.10.6)(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.101.0)(terser@5.49.0)(tsx@4.23.1)(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) + 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)) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 - '@tokenizer/inflate@0.4.1': + '@tokenizer/inflate@0.4.1(supports-color@8.1.1)': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) token-types: 6.1.2 transitivePeerDependencies: - supports-color '@tokenizer/token@0.3.0': {} - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)))(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.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))': dependencies: '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 javascript-natural-sort: 0.7.1 lodash-es: 4.18.1 minimatch: 9.0.9 parse-imports-exports: 0.2.4 - prettier: 3.9.5 + prettier: 3.9.6 optionalDependencies: - prettier-plugin-svelte: 4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + 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) transitivePeerDependencies: - supports-color @@ -18510,15 +18375,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@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)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/visitor-keys': 8.64.0 - eslint: 10.7.0(jiti@2.7.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-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 + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) @@ -18526,58 +18391,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.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)': dependencies: - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - eslint: 10.7.0(jiti@2.7.0) + '@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 + 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.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/project-service@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - debug: 4.4.3 + '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.64.0': + '@typescript-eslint/scope-manager@8.65.0': dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 - '@typescript-eslint/tsconfig-utils@8.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.65.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.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)': dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - debug: 4.4.3 - eslint: 10.7.0(jiti@2.7.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/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) + 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) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.64.0': {} + '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/typescript-estree@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/project-service': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - minimatch: 10.2.5 + '@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 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) @@ -18585,20 +18450,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@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)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - eslint: 10.7.0(jiti@2.7.0) + '@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) + 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.64.0': + '@typescript-eslint/visitor-keys@8.65.0': dependencies: - '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 '@typescript/typescript-aix-ppc64@7.0.2': @@ -18676,7 +18541,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.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(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.1)(yaml@2.9.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.10 @@ -18688,7 +18553,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.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(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.1)(yaml@2.9.0) '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: @@ -18702,7 +18567,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.10.6)(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.101.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)) '@vitest/expect@3.2.7': dependencies: @@ -18721,21 +18586,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.101.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.1)(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.101.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.1)(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.101.0)(terser@5.49.0)(tsx@4.23.1)(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))': 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.101.0)(terser@5.49.0)(tsx@4.23.1)(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) '@vitest/pretty-format@3.2.7': dependencies: @@ -18871,10 +18736,10 @@ snapshots: dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.9(svelte@5.56.5(@typescript-eslint/types@8.64.0))': + '@zoom-image/svelte@0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0))': dependencies: '@zoom-image/core': 0.42.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) abbrev@1.1.1: {} @@ -18894,29 +18759,29 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-attributes@1.9.5(acorn@8.17.0): + acorn-import-attributes@1.9.5(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn-import-phases@1.0.4(acorn@8.17.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk@8.3.5: dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn@8.17.0: {} + acorn@8.18.0: {} address@2.0.3: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18959,14 +18824,14 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -19121,59 +18986,59 @@ snapshots: dependencies: immediate: 3.3.0 - autoprefixer@10.5.4(postcss@8.5.19): + autoprefixer@10.5.4(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-lite: 1.0.30001806 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.19 + postcss: 8.5.25 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)(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: - '@babel/core': 7.29.7 + '@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(postcss@8.5.19) + 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-plugin-dynamic-import-node@2.3.3: dependencies: object.assign: 4.1.7 - babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7): + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -19240,15 +19105,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.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)): dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/dom': 1.7.6 + '@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.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + 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)) tabbable: 6.5.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -19259,11 +19124,11 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.6: + body-parser@1.20.6(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -19276,11 +19141,11 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.3.0: + body-parser@2.3.0(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 @@ -19328,7 +19193,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.7: + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -19369,10 +19234,10 @@ snapshots: builtin-modules@5.2.0: {} - bullmq@5.80.5: + bullmq@5.81.2(supports-color@8.1.1): dependencies: cron-parser: 4.9.0 - ioredis: 5.11.1 + ioredis: 5.11.1(supports-color@8.1.1) msgpackr: 2.0.4 node-abort-controller: 3.1.1 semver: 7.8.5 @@ -19461,9 +19326,9 @@ snapshots: ccount@2.0.1: {} - ce-la-react@0.3.2(react@19.2.7): + ce-la-react@0.3.2(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 chai@5.3.3: dependencies: @@ -19623,11 +19488,11 @@ snapshots: cluster-key-slot@1.1.1: {} - codemirror-wrapped-line-indent@1.0.9(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1): + codemirror-wrapped-line-indent@1.0.9(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7): dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 collapse-white-space@2.1.0: {} @@ -19696,11 +19561,11 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.1: + compression@1.8.1(supports-color@8.1.1): dependencies: bytes: 3.1.2 compressible: 2.0.18 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -19792,7 +19657,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -19800,7 +19665,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(postcss@8.5.19) + 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) core-js-compat@3.49.0: dependencies: @@ -19854,7 +19719,7 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - crelt@1.0.6: {} + crelt@1.0.7: {} cron-parser@4.9.0: dependencies: @@ -19875,50 +19740,53 @@ snapshots: dependencies: type-fest: 1.4.0 - css-blank-pseudo@7.0.1(postcss@8.5.19): + css-blank-pseudo@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - css-declaration-sorter@7.4.0(postcss@8.5.19): + css-declaration-sorter@7.4.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - css-has-pseudo@7.0.3(postcss@8.5.19): + css-has-pseudo@7.0.3(postcss@8.5.25): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: - icss-utils: 5.1.0(postcss@8.5.19) - postcss: 8.5.19 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.19) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.19) - postcss-modules-scope: 3.2.1(postcss@8.5.19) - postcss-modules-values: 4.0.0(postcss@8.5.19) + 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) postcss-value-parser: 4.2.0 semver: 7.8.5 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + 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)(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 6.1.2(postcss@8.5.19) + cssnano: 6.1.2(postcss@8.5.25) jest-worker: 29.7.0 - postcss: 8.5.19 + postcss: 8.5.25 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(postcss@8.5.19) + 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) optionalDependencies: clean-css: 5.3.3 + csso: 5.0.5 + esbuild: 0.28.1 + lightningcss: 1.33.0 - css-prefers-color-scheme@10.0.0(postcss@8.5.19): + css-prefers-color-scheme@10.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 css-select@4.3.0: dependencies: @@ -19956,60 +19824,60 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-advanced@6.1.2(postcss@8.5.19): + cssnano-preset-advanced@6.1.2(postcss@8.5.25): dependencies: - autoprefixer: 10.5.4(postcss@8.5.19) + autoprefixer: 10.5.4(postcss@8.5.25) browserslist: 4.28.6 - cssnano-preset-default: 6.1.2(postcss@8.5.19) - postcss: 8.5.19 - postcss-discard-unused: 6.0.5(postcss@8.5.19) - postcss-merge-idents: 6.0.3(postcss@8.5.19) - postcss-reduce-idents: 6.0.3(postcss@8.5.19) - postcss-zindex: 6.0.2(postcss@8.5.19) + 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.19): + cssnano-preset-default@6.1.2(postcss@8.5.25): dependencies: browserslist: 4.28.6 - css-declaration-sorter: 7.4.0(postcss@8.5.19) - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 - postcss-calc: 9.0.1(postcss@8.5.19) - postcss-colormin: 6.1.0(postcss@8.5.19) - postcss-convert-values: 6.1.0(postcss@8.5.19) - postcss-discard-comments: 6.0.2(postcss@8.5.19) - postcss-discard-duplicates: 6.0.3(postcss@8.5.19) - postcss-discard-empty: 6.0.3(postcss@8.5.19) - postcss-discard-overridden: 6.0.2(postcss@8.5.19) - postcss-merge-longhand: 6.0.5(postcss@8.5.19) - postcss-merge-rules: 6.1.1(postcss@8.5.19) - postcss-minify-font-values: 6.1.0(postcss@8.5.19) - postcss-minify-gradients: 6.0.3(postcss@8.5.19) - postcss-minify-params: 6.1.0(postcss@8.5.19) - postcss-minify-selectors: 6.0.4(postcss@8.5.19) - postcss-normalize-charset: 6.0.2(postcss@8.5.19) - postcss-normalize-display-values: 6.0.2(postcss@8.5.19) - postcss-normalize-positions: 6.0.2(postcss@8.5.19) - postcss-normalize-repeat-style: 6.0.2(postcss@8.5.19) - postcss-normalize-string: 6.0.2(postcss@8.5.19) - postcss-normalize-timing-functions: 6.0.2(postcss@8.5.19) - postcss-normalize-unicode: 6.1.0(postcss@8.5.19) - postcss-normalize-url: 6.0.2(postcss@8.5.19) - postcss-normalize-whitespace: 6.0.2(postcss@8.5.19) - postcss-ordered-values: 6.0.2(postcss@8.5.19) - postcss-reduce-initial: 6.1.0(postcss@8.5.19) - postcss-reduce-transforms: 6.0.2(postcss@8.5.19) - postcss-svgo: 6.0.3(postcss@8.5.19) - postcss-unique-selectors: 6.0.4(postcss@8.5.19) + 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) - cssnano-utils@4.0.2(postcss@8.5.19): + cssnano-utils@4.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - cssnano@6.1.2(postcss@8.5.19): + cssnano@6.1.2(postcss@8.5.25): dependencies: - cssnano-preset-default: 6.1.2(postcss@8.5.19) + cssnano-preset-default: 6.1.2(postcss@8.5.25) lilconfig: 3.1.3 - postcss: 8.5.19 + postcss: 8.5.25 csso@5.0.5: dependencies: @@ -20230,17 +20098,28 @@ snapshots: debounce@2.2.0: {} - debug@2.6.9: + debug@2.6.9(supports-color@8.1.1): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 8.1.1 - debug@4.3.7: + debug@4.3.7(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 debug@4.4.3: dependencies: ms: 2.1.3 + optional: true + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decamelize@1.2.0: {} @@ -20323,7 +20202,7 @@ snapshots: dependencies: address: 2.0.3 - devalue@5.8.1: {} + devalue@5.8.2: {} devlop@1.1.0: dependencies: @@ -20360,29 +20239,29 @@ snapshots: dependencies: yaml: 2.9.0 - docker-modem@5.0.7: + docker-modem@5.0.7(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.17.0 transitivePeerDependencies: - supports-color - dockerode@5.0.1: + dockerode@5.0.1(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@grpc/grpc-js': 1.14.4 '@grpc/proto-loader': 0.7.15 - docker-modem: 5.0.7 + docker-modem: 5.0.7(supports-color@8.1.1) protobufjs: 7.6.5 tar-fs: 2.1.5 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.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + 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): dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@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) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -20393,8 +20272,8 @@ snapshots: lunr-languages: 1.20.0 mark.js: 8.11.1 minimatch: 3.1.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) rehype-parse: 7.0.1 to-vfile: 6.1.0 unified: 9.2.2 @@ -20501,10 +20380,10 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.5: + engine.io-client@6.6.5(supports-color@8.1.1): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.20.1 xmlhttprequest-ssl: 2.1.2 @@ -20515,7 +20394,7 @@ snapshots: engine.io-parser@5.2.3: {} - engine.io@6.6.8: + engine.io@6.6.8(supports-color@8.1.1): dependencies: '@types/cors': 2.8.19 '@types/node': 24.13.3 @@ -20524,7 +20403,7 @@ snapshots: base64id: 2.0.0 cookie: 0.7.2 cors: 2.8.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.20.1 transitivePeerDependencies: @@ -20532,12 +20411,7 @@ snapshots: - supports-color - utf-8-validate - enhanced-resolve@5.21.6: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - enhanced-resolve@5.24.2: + enhanced-resolve@5.24.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -20614,7 +20488,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.17.0 + acorn: 8.18.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -20714,77 +20588,77 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) - eslint-plugin-better-tailwindcss@4.6.1(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))(tailwindcss@4.3.2): + eslint-plugin-better-tailwindcss@4.7.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(tailwindcss@4.3.3): dependencies: - '@eslint/css-tree': 4.0.4 + '@eslint/css-tree': 4.0.5 '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(@typescript/typescript6@6.0.2)) - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 jiti: 2.7.0 synckit: 0.11.13 tailwind-csstree: 0.3.3 - tailwindcss: 4.3.2 + tailwindcss: 4.3.3 tsconfig-paths-webpack-plugin: 4.2.0 valibot: 1.4.2(@typescript/typescript6@6.0.2) optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) transitivePeerDependencies: - '@eslint/css' - typescript - eslint-plugin-compat@7.0.2(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-compat@7.0.2(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: '@mdn/browser-compat-data': 6.1.5 ast-metadata-inferer: 0.8.1 browserslist: 4.28.6 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 semver: 7.8.5 - eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5): + eslint-plugin-prettier@5.5.6(@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@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6): dependencies: - eslint: 10.7.0(jiti@2.7.0) - prettier: 3.9.5 + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) + prettier: 3.9.6 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) - eslint-plugin-svelte@3.20.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.5(@typescript-eslint/types@8.64.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.65.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@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 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 - postcss: 8.5.19 - postcss-load-config: 3.1.4(postcss@8.5.19) - postcss-safe-parser: 7.0.1(postcss@8.5.19) + postcss: 8.5.25 + postcss-load-config: 3.1.4(postcss@8.5.25) + postcss-safe-parser: 7.0.1(postcss@8.5.25) semver: 7.8.5 - svelte-eslint-parser: 1.8.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) optionalDependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) transitivePeerDependencies: - ts-node - eslint-plugin-unicorn@72.0.0(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-unicorn@72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) - '@eslint/css-tree': 4.0.4 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) + '@eslint/css-tree': 4.0.5 browserslist: 4.28.6 change-case: 5.4.4 ci-info: 4.4.0 core-js-compat: 3.49.0 detect-indent: 7.0.2 entities: 4.5.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) find-up-simple: 1.0.1 globals: 17.7.0 indent-string: 5.0.0 @@ -20821,12 +20695,12 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.7.0(jiti@2.7.0): + eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.6.0 + '@eslint/config-array': 0.23.5(supports-color@8.1.1) + '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 @@ -20835,7 +20709,7 @@ snapshots: '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -20850,7 +20724,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -20869,14 +20743,14 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 4.2.1 espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esprima@4.0.1: {} @@ -20885,11 +20759,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.13(@typescript-eslint/types@8.64.0): + esrap@2.2.13(@typescript-eslint/types@8.65.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 optionalDependencies: - '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/types': 8.65.0 esrecurse@4.3.0: dependencies: @@ -21003,21 +20877,21 @@ snapshots: exponential-backoff@3.1.3: {} - express@4.22.2: + express@4.22.2(supports-color@8.1.1): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.6 + body-parser: 1.20.6(supports-color@8.1.1) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.2 + finalhandler: 1.3.2(supports-color@8.1.1) fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 @@ -21029,8 +20903,8 @@ snapshots: qs: 6.15.3 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 + send: 0.19.2(supports-color@8.1.1) + serve-static: 1.16.3(supports-color@8.1.1) setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -21039,20 +20913,20 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.2.1(supports-color@8.1.1): dependencies: accepts: 2.0.0 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@8.1.1) content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@8.1.1) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -21063,9 +20937,9 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.3 range-parser: 1.3.0 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 + router: 2.2.0(supports-color@8.1.1) + send: 1.2.1(supports-color@8.1.1) + serve-static: 2.2.1(supports-color@8.1.1) statuses: 2.0.2 type-is: 2.1.0 vary: 1.1.2 @@ -21082,10 +20956,10 @@ snapshots: extend@3.0.2: {} - fabric@7.4.0: + fabric@7.4.0(supports-color@8.1.1): optionalDependencies: canvas: 3.2.3 - jsdom: 26.1.0(canvas@3.2.3) + jsdom: 26.1.0(canvas@3.2.3)(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color @@ -21116,7 +20990,7 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.3: {} + fast-uri@3.1.4: {} fastq@1.20.1: dependencies: @@ -21148,19 +21022,19 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + 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-source@0.6.1: dependencies: stream-source: 0.3.5 - file-type@21.3.4: + file-type@21.3.4(supports-color@8.1.1): dependencies: - '@tokenizer/inflate': 0.4.1 + '@tokenizer/inflate': 0.4.1(supports-color@8.1.1) strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 @@ -21171,9 +21045,9 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.2: + finalhandler@1.3.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -21183,9 +21057,9 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.1: + finalhandler@2.1.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -21218,26 +21092,28 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.3 keyv: 4.5.4 flat@5.0.2: {} - flatted@3.4.2: {} + flatted@3.4.3: {} fluent-ffmpeg@2.1.3: dependencies: async: 0.2.10 which: 1.3.1 - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@8.1.1)): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) foreground-child@3.3.1: dependencies: 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.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.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)): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 @@ -21252,7 +21128,7 @@ snapshots: semver: 7.8.5 tapable: 2.3.3 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.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) form-data-encoder@2.1.4: {} @@ -21411,7 +21287,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 path-scurry: 2.0.2 @@ -21496,7 +21372,7 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.10.6: + happy-dom@20.11.1: dependencies: '@types/node': 24.13.3 '@types/whatwg-mimetype': 3.0.2 @@ -21592,7 +21468,7 @@ snapshots: unist-util-visit: 2.0.3 zwitch: 1.0.5 - hast-util-to-estree@3.1.3: + hast-util-to-estree@3.1.3(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -21602,9 +21478,9 @@ snapshots: estree-util-attach-comments: 3.0.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -21613,7 +21489,7 @@ snapshots: transitivePeerDependencies: - supports-color - hast-util-to-jsx-runtime@2.3.6: + hast-util-to-jsx-runtime@2.3.6(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 @@ -21622,9 +21498,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -21751,7 +21627,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.7(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -21759,7 +21635,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + 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) html5parser@3.0.0: {} @@ -21812,10 +21688,18 @@ snapshots: - supports-color optional: true - http-proxy-middleware@2.0.10(@types/express@4.17.25): + http-proxy-agent@7.0.2(supports-color@8.1.1): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + optional: true + + http-proxy-middleware@2.0.10(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)): dependencies: '@types/http-proxy': 1.17.17 - http-proxy: 1.18.1 + http-proxy: 1.18.1(debug@4.4.3(supports-color@8.1.1)) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -21824,10 +21708,10 @@ snapshots: transitivePeerDependencies: - debug - http-proxy@1.18.1: + http-proxy@1.18.1(debug@4.4.3(supports-color@8.1.1)): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -21837,10 +21721,10 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@8.1.1): dependencies: - agent-base: 6.0.2 - debug: 4.4.3 + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -21852,6 +21736,14 @@ snapshots: - supports-color optional: true + https-proxy-agent@7.0.6(supports-color@8.1.1): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + optional: true + human-signals@2.1.0: {} hyperdyperid@1.2.0: {} @@ -21872,9 +21764,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.19): + icss-utils@5.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 identifier-regex@1.1.0: dependencies: @@ -21892,7 +21784,7 @@ snapshots: immediate@3.3.0: {} - immutable-json-patch@6.0.2: {} + immutable-json-patch@6.0.3: {} immutable@5.1.6: {} @@ -21903,8 +21795,8 @@ snapshots: import-in-the-middle@3.0.2: dependencies: - acorn: 8.17.0 - acorn-import-attributes: 1.9.5(acorn@8.17.0) + acorn: 8.18.0 + acorn-import-attributes: 1.9.5(acorn@8.18.0) cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 @@ -21973,11 +21865,11 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis@5.11.1: + ioredis@5.11.1(supports-color@8.1.1): dependencies: '@ioredis/commands': 1.10.0 cluster-key-slot: 1.1.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) denque: 2.1.0 redis-errors: 1.2.0 redis-parser: 3.0.0 @@ -22182,7 +22074,7 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@6.2.3: {} + jose@6.2.4: {} js-tokens@10.0.0: {} @@ -22194,6 +22086,14 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@5.2.1: + dependencies: + argparse: 2.0.1 + + js-yaml@5.2.2: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0(canvas@3.2.3): dependencies: cssstyle: 4.6.0 @@ -22224,6 +22124,36 @@ snapshots: - utf-8-validate optional: true + jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.24 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.1 + xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 3.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + jsep@1.4.0: {} jsesc@3.1.0: {} @@ -22260,7 +22190,7 @@ snapshots: '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) jsep: 1.4.0 - jsonrepair@3.14.0: {} + jsonrepair@3.15.0: {} jsonwebtoken@9.0.3: dependencies: @@ -22640,13 +22570,13 @@ snapshots: math-intrinsics@1.1.0: {} - mdast-util-directive@3.1.0: + mdast-util-directive@3.1.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -22661,14 +22591,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3: + mdast-util-from-markdown@2.0.3(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@8.1.1) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -22678,12 +22608,12 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-frontmatter@2.0.1: + mdast-util-frontmatter@2.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 escape-string-regexp: 5.0.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 micromark-extension-frontmatter: 2.0.0 transitivePeerDependencies: @@ -22697,67 +22627,67 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.1.0: + mdast-util-gfm-footnote@2.1.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@2.0.0: + mdast-util-gfm-strikethrough@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-table@2.0.0: + mdast-util-gfm-table@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@2.0.0: + mdast-util-gfm-task-list-item@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0: + mdast-util-gfm@3.1.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-gfm-footnote: 2.1.0(supports-color@8.1.1) + mdast-util-gfm-strikethrough: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-table: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-task-list-item: 2.0.0(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1: + mdast-util-mdx-expression@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0: + mdast-util-mdx-jsx@3.2.0(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -22765,7 +22695,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -22774,23 +22704,23 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx@3.0.0: + mdast-util-mdx@3.0.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1: + mdast-util-mdxjs-esm@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -22832,11 +22762,11 @@ snapshots: mdn-data@2.0.30: {} - mdn-data@2.28.1: {} + mdn-data@2.29.0: {} - media-chrome@4.19.2(react@19.2.7): + media-chrome@4.19.2(react@19.2.8): dependencies: - ce-la-react: 0.3.2(react@19.2.7) + ce-la-react: 0.3.2(react@19.2.8) transitivePeerDependencies: - react @@ -23050,8 +22980,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 @@ -23187,10 +23117,10 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@8.1.1): dependencies: '@types/debug': 4.1.13 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -23246,17 +23176,17 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.2(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 - webpack: 5.108.4(postcss@8.5.19) + 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) minimalistic-assert@1.0.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.8 minimatch@3.1.5: dependencies: @@ -23272,28 +23202,23 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): + 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)): 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(postcss@8.5.19) + 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) optionalDependencies: + '@swc/core': 1.15.46(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.19) + cssnano: 6.1.2(postcss@8.5.25) + csso: 5.0.5 + esbuild: 0.28.1 html-minifier-terser: 7.2.0 - postcss: 8.5.19 - - minimizer-webpack-plugin@5.6.1(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): - 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(postcss@8.5.19) - optionalDependencies: - postcss: 8.5.19 + lightningcss: 1.33.0 + postcss: 8.5.25 + uglify-js: 3.19.3 minipass@3.3.6: dependencies: @@ -23385,7 +23310,7 @@ snapshots: nanoid@3.3.16: {} - nanoid@5.1.16: {} + nanoid@6.0.0: {} napi-build-utils@2.0.0: optional: true @@ -23409,12 +23334,12 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2): + nest-commander@3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@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) '@types/inquirer': 8.2.13 commander: 11.1.0 cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) @@ -23423,38 +23348,38 @@ snapshots: - '@types/node' - typescript - nestjs-cls@6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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) kysely: 0.28.17 reflect-metadata: 0.2.2 tslib: 2.8.1 - nestjs-otel@8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(rxjs@7.8.2): + nestjs-otel@8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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) '@opentelemetry/api': 1.9.1 '@opentelemetry/host-metrics': 0.38.3(@opentelemetry/api@1.9.1) rxjs: 7.8.2 tslib: 2.8.1 - nestjs-zod@5.4.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + nestjs-zod@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): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) deepmerge: 4.3.1 rxjs: 7.8.2 zod: 4.3.6 optionalDependencies: - '@nestjs/swagger': 11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2) + '@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) next-tick@1.1.0: {} @@ -23553,11 +23478,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + 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) nwsapi@2.2.24: optional: true @@ -23593,18 +23518,18 @@ snapshots: obug@2.1.3: {} - oidc-provider@9.9.1: + oidc-provider@9.10.0(supports-color@8.1.1): dependencies: '@koa/cors': 5.0.0 - '@koa/router': 15.7.0(koa@3.2.1) - debug: 4.4.3 + '@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.3 + jose: 6.2.4 jsesc: 3.1.0 koa: 3.2.1 - nanoid: 5.1.16 + nanoid: 6.0.0 quick-lru: 7.3.0 - raw-body: 3.0.2 + raw-body: 4.0.0 transitivePeerDependencies: - supports-color @@ -23652,7 +23577,7 @@ snapshots: openid-client@6.8.4: dependencies: - jose: 6.2.3 + jose: 6.2.4 oauth4webapi: 3.8.6 optionator@0.9.4: @@ -23931,11 +23856,11 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 - playwright-core@1.61.1: {} + playwright-core@1.62.0: {} - playwright@1.61.1: + playwright@1.62.0: dependencies: - playwright-core: 1.61.1 + playwright-core: 1.62.0 optionalDependencies: fsevents: 2.3.2 @@ -23969,448 +23894,448 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-attribute-case-insensitive@7.0.1(postcss@8.5.19): + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-calc@9.0.1(postcss@8.5.19): + postcss-calc@9.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.19): + postcss-clamp@4.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@7.0.12(postcss@8.5.19): + postcss-color-functional-notation@7.0.12(postcss@8.5.25): 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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 - postcss-color-hex-alpha@10.0.0(postcss@8.5.19): + postcss-color-hex-alpha@10.0.0(postcss@8.5.25): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@10.0.0(postcss@8.5.19): + postcss-color-rebeccapurple@10.0.0(postcss@8.5.25): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.5.19): + postcss-colormin@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.5.19): + postcss-convert-values@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-custom-media@11.0.6(postcss@8.5.19): + postcss-custom-media@11.0.6(postcss@8.5.25): 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.19 + postcss: 8.5.25 - postcss-custom-properties@14.0.6(postcss@8.5.19): + postcss-custom-properties@14.0.6(postcss@8.5.25): 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.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-custom-selectors@8.0.5(postcss@8.5.19): + postcss-custom-selectors@8.0.5(postcss@8.5.25): 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.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-dir-pseudo-class@9.0.1(postcss@8.5.19): + postcss-dir-pseudo-class@9.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-discard-comments@6.0.2(postcss@8.5.19): + postcss-discard-comments@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-duplicates@6.0.3(postcss@8.5.19): + postcss-discard-duplicates@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-empty@6.0.3(postcss@8.5.19): + postcss-discard-empty@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-overridden@6.0.2(postcss@8.5.19): + postcss-discard-overridden@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-unused@6.0.5(postcss@8.5.19): + postcss-discard-unused@6.0.5(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-double-position-gradients@6.0.4(postcss@8.5.19): + postcss-double-position-gradients@6.0.4(postcss@8.5.25): dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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 postcss-value-parser: 4.2.0 - postcss-focus-visible@10.0.1(postcss@8.5.19): + postcss-focus-visible@10.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-focus-within@9.0.1(postcss@8.5.19): + postcss-focus-within@9.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-font-variant@5.0.0(postcss@8.5.19): + postcss-font-variant@5.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-gap-properties@6.0.0(postcss@8.5.19): + postcss-gap-properties@6.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-image-set-function@7.0.0(postcss@8.5.19): + postcss-image-set-function@7.0.0(postcss@8.5.25): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-import@15.1.0(postcss@8.5.19): + postcss-import@15.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.19): + postcss-js@4.1.0(postcss@8.5.25): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.19 + postcss: 8.5.25 - postcss-lab-function@7.0.12(postcss@8.5.19): + postcss-lab-function@7.0.12(postcss@8.5.25): 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.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@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 - postcss-load-config@3.1.4(postcss@8.5.19): + postcss-load-config@3.1.4(postcss@8.5.25): dependencies: lilconfig: 2.1.0 yaml: 1.10.3 optionalDependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.19)(tsx@4.23.1)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.25)(tsx@4.23.1)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.19 + postcss: 8.5.25 tsx: 4.23.1 yaml: 2.9.0 - postcss-loader@7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) jiti: 1.21.7 - postcss: 8.5.19 + postcss: 8.5.25 semver: 7.8.5 - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - typescript - postcss-logical@8.1.0(postcss@8.5.19): + postcss-logical@8.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-merge-idents@6.0.3(postcss@8.5.19): + postcss-merge-idents@6.0.3(postcss@8.5.25): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-merge-longhand@6.0.5(postcss@8.5.19): + postcss-merge-longhand@6.0.5(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.5.19) + stylehacks: 6.1.1(postcss@8.5.25) - postcss-merge-rules@6.1.1(postcss@8.5.19): + postcss-merge-rules@6.1.1(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-minify-font-values@6.1.0(postcss@8.5.19): + postcss-minify-font-values@6.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.5.19): + postcss-minify-gradients@6.0.3(postcss@8.5.25): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.5.19): + postcss-minify-params@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.5.19): + postcss-minify-selectors@6.0.4(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-modules-extract-imports@3.1.0(postcss@8.5.19): + postcss-modules-extract-imports@3.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-modules-local-by-default@4.2.0(postcss@8.5.19): + postcss-modules-local-by-default@4.2.0(postcss@8.5.25): dependencies: - icss-utils: 5.1.0(postcss@8.5.19) - postcss: 8.5.19 + icss-utils: 5.1.0(postcss@8.5.25) + postcss: 8.5.25 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.19): + postcss-modules-scope@3.2.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-modules-values@4.0.0(postcss@8.5.19): + postcss-modules-values@4.0.0(postcss@8.5.25): dependencies: - icss-utils: 5.1.0(postcss@8.5.19) - postcss: 8.5.19 + icss-utils: 5.1.0(postcss@8.5.25) + postcss: 8.5.25 - postcss-nested@6.2.0(postcss@8.5.19): + postcss-nested@6.2.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-nesting@13.0.2(postcss@8.5.19): + postcss-nesting@13.0.2(postcss@8.5.25): 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.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-normalize-charset@6.0.2(postcss@8.5.19): + postcss-normalize-charset@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-normalize-display-values@6.0.2(postcss@8.5.19): + postcss-normalize-display-values@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.5.19): + postcss-normalize-positions@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.5.19): + postcss-normalize-repeat-style@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.5.19): + postcss-normalize-string@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.5.19): + postcss-normalize-timing-functions@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.5.19): + postcss-normalize-unicode@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.5.19): + postcss-normalize-url@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.5.19): + postcss-normalize-whitespace@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@3.0.0(postcss@8.5.19): + postcss-opacity-percentage@3.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-ordered-values@6.0.2(postcss@8.5.19): + postcss-ordered-values@6.0.2(postcss@8.5.25): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@6.0.0(postcss@8.5.19): + postcss-overflow-shorthand@6.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.19): + postcss-page-break@3.0.4(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-place@10.0.0(postcss@8.5.19): + postcss-place@10.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-preset-env@10.6.1(postcss@8.5.19): + postcss-preset-env@10.6.1(postcss@8.5.25): dependencies: - '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.19) - '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.19) - '@csstools/postcss-color-function': 4.0.12(postcss@8.5.19) - '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.19) - '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.19) - '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.19) - '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.19) - '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.19) - '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.19) - '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.19) - '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.19) - '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.19) - '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.19) - '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.19) - '@csstools/postcss-initial': 2.0.1(postcss@8.5.19) - '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.19) - '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.19) - '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.19) - '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.19) - '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.19) - '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.19) - '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.19) - '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.19) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.19) - '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.19) - '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.19) - '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.19) - '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.19) - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.19) - '@csstools/postcss-random-function': 2.0.1(postcss@8.5.19) - '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.19) - '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.19) - '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.19) - '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.19) - '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.19) - '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.19) - '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.19) - '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.19) - '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.19) - autoprefixer: 10.5.4(postcss@8.5.19) + '@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) browserslist: 4.28.6 - css-blank-pseudo: 7.0.1(postcss@8.5.19) - css-has-pseudo: 7.0.3(postcss@8.5.19) - css-prefers-color-scheme: 10.0.0(postcss@8.5.19) + 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) cssdb: 8.9.0 - postcss: 8.5.19 - postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.19) - postcss-clamp: 4.1.0(postcss@8.5.19) - postcss-color-functional-notation: 7.0.12(postcss@8.5.19) - postcss-color-hex-alpha: 10.0.0(postcss@8.5.19) - postcss-color-rebeccapurple: 10.0.0(postcss@8.5.19) - postcss-custom-media: 11.0.6(postcss@8.5.19) - postcss-custom-properties: 14.0.6(postcss@8.5.19) - postcss-custom-selectors: 8.0.5(postcss@8.5.19) - postcss-dir-pseudo-class: 9.0.1(postcss@8.5.19) - postcss-double-position-gradients: 6.0.4(postcss@8.5.19) - postcss-focus-visible: 10.0.1(postcss@8.5.19) - postcss-focus-within: 9.0.1(postcss@8.5.19) - postcss-font-variant: 5.0.0(postcss@8.5.19) - postcss-gap-properties: 6.0.0(postcss@8.5.19) - postcss-image-set-function: 7.0.0(postcss@8.5.19) - postcss-lab-function: 7.0.12(postcss@8.5.19) - postcss-logical: 8.1.0(postcss@8.5.19) - postcss-nesting: 13.0.2(postcss@8.5.19) - postcss-opacity-percentage: 3.0.0(postcss@8.5.19) - postcss-overflow-shorthand: 6.0.0(postcss@8.5.19) - postcss-page-break: 3.0.4(postcss@8.5.19) - postcss-place: 10.0.0(postcss@8.5.19) - postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.19) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.19) - postcss-selector-not: 8.0.1(postcss@8.5.19) + 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-pseudo-class-any-link@10.0.1(postcss@8.5.19): + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-reduce-idents@6.0.3(postcss@8.5.19): + postcss-reduce-idents@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.5.19): + postcss-reduce-initial@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - postcss: 8.5.19 + postcss: 8.5.25 - postcss-reduce-transforms@6.0.2(postcss@8.5.19): + postcss-reduce-transforms@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.19): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-safe-parser@7.0.1(postcss@8.5.19): + postcss-safe-parser@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-scss@4.0.9(postcss@8.5.19): + postcss-scss@4.0.9(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-selector-not@8.0.1(postcss@8.5.19): + postcss-selector-not@8.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 postcss-selector-parser@6.1.4: @@ -24423,29 +24348,29 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sort-media-queries@5.2.0(postcss@8.5.19): + postcss-sort-media-queries@5.2.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 sort-css-media-queries: 2.2.0 - postcss-svgo@6.0.3(postcss@8.5.19): + postcss-svgo@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 svgo: 3.3.4 - postcss-unique-selectors@6.0.4(postcss@8.5.19): + postcss-unique-selectors@6.0.4(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 postcss-value-parser@4.2.0: {} - postcss-zindex@6.0.2(postcss@8.5.19): + postcss-zindex@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss@8.5.19: + postcss@8.5.25: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 @@ -24489,21 +24414,21 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5): + prettier-plugin-organize-imports@4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6): dependencies: - prettier: 3.9.5 + prettier: 3.9.6 typescript: '@typescript/typescript6@6.0.2' - prettier-plugin-sort-json@4.2.0(prettier@3.9.5): + prettier-plugin-sort-json@4.2.0(prettier@3.9.6): dependencies: - prettier: 3.9.5 + prettier: 3.9.6 - prettier-plugin-svelte@4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - prettier: 3.9.5 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + prettier: 3.9.6 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - prettier@3.9.5: {} + prettier@3.9.6: {} pretty-error@4.0.0: dependencies: @@ -24518,11 +24443,11 @@ snapshots: pretty-time@1.1.0: {} - prism-react-renderer@2.4.1(react@19.2.7): + prism-react-renderer@2.4.1(react@19.2.8): dependencies: '@types/prismjs': 1.26.6 clsx: 2.1.1 - react: 19.2.7 + react: 19.2.8 prismjs@1.30.0: {} @@ -24549,9 +24474,9 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 - properties-reader@3.0.1: + properties-reader@3.0.1(supports-color@8.1.1): dependencies: - '@kwsites/file-exists': 1.1.1 + '@kwsites/file-exists': 1.1.1(supports-color@8.1.1) mkdirp: 3.0.1 transitivePeerDependencies: - supports-color @@ -24658,11 +24583,16 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 - raw-loader@4.0.2(webpack@5.108.4(postcss@8.5.19)): + raw-body@4.0.0: + dependencies: + 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)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + 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) rc@1.2.8: dependencies: @@ -24671,15 +24601,15 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-dom@19.2.7(react@19.2.7): + react-dom@19.2.8(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 scheduler: 0.27.0 - react-email@5.2.11: + react-email@5.2.11(supports-color@8.1.1): dependencies: '@babel/parser': 7.27.0 - '@babel/traverse': 7.27.0 + '@babel/traverse': 7.27.0(supports-color@8.1.1) chokidar: 4.0.3 commander: 13.1.0 conf: 15.1.0 @@ -24693,7 +24623,7 @@ snapshots: nypm: 0.6.5 ora: 8.2.0 prompts: 2.4.2 - socket.io: 4.8.3 + socket.io: 4.8.3(supports-color@8.1.1) tsconfig-paths: 4.2.0 transitivePeerDependencies: - bufferutil @@ -24706,34 +24636,34 @@ snapshots: react-is@17.0.2: {} - react-json-view-lite@2.5.0(react@19.2.7): + react-json-view-lite@2.5.0(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 - react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: '@babel/runtime': 7.29.7 - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' - webpack: 5.108.4(postcss@8.5.19) + 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) - react-router-config@5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7): + react-router-config@5.1.1(react-router@5.3.4(react@19.2.8))(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 - react: 19.2.7 - react-router: 5.3.4(react@19.2.7) + react: 19.2.8 + react-router: 5.3.4(react@19.2.8) - react-router-dom@5.3.4(react@19.2.7): + react-router-dom@5.3.4(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.7 - react-router: 5.3.4(react@19.2.7) + react: 19.2.8 + react-router: 5.3.4(react@19.2.8) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react-router@5.3.4(react@19.2.7): + react-router@5.3.4(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 history: 4.10.1 @@ -24741,12 +24671,12 @@ snapshots: loose-envify: 1.4.0 path-to-regexp: 1.9.0 prop-types: 15.8.1 - react: 19.2.7 + react: 19.2.8 react-is: 16.13.1 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react@19.2.7: {} + react@19.2.8: {} read-cache@1.0.0: dependencies: @@ -24794,10 +24724,10 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.3 - recma-jsx@1.0.1(acorn@8.17.0): + recma-jsx@1.0.1(acorn@8.18.0): dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 @@ -24870,20 +24800,20 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 - rehype-recma@1.0.0: + rehype-recma@1.0.0(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 - hast-util-to-estree: 3.1.3 + hast-util-to-estree: 3.1.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color relateurl@0.2.7: {} - remark-directive@3.0.1: + remark-directive@3.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-directive: 3.1.0 + mdast-util-directive: 3.1.0(supports-color@8.1.1) micromark-extension-directive: 3.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -24897,37 +24827,37 @@ snapshots: node-emoji: 2.2.0 unified: 11.0.5 - remark-frontmatter@5.0.0: + remark-frontmatter@5.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-frontmatter: 2.0.1 + mdast-util-frontmatter: 2.0.1(supports-color@8.1.1) micromark-extension-frontmatter: 2.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-gfm@4.0.1: + remark-gfm@4.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 + mdast-util-gfm: 3.1.0(supports-color@8.1.1) micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@8.1.1) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-mdx@3.1.1: + remark-mdx@3.1.1(supports-color@8.1.1): dependencies: - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@8.1.1) micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - remark-parse@11.0.0: + remark-parse@11.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -24961,9 +24891,9 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@8.0.1: + require-in-the-middle@8.0.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.4 transitivePeerDependencies: - supports-color @@ -25092,9 +25022,9 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 - router@2.2.0: + router@2.2.0(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -25109,7 +25039,7 @@ snapshots: dependencies: escalade: 3.2.0 picocolors: 1.1.1 - postcss: 8.5.19 + postcss: 8.5.25 strip-json-comments: 3.1.1 run-applescript@7.1.0: {} @@ -25120,14 +25050,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + 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)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) optionalDependencies: - '@sveltejs/kit': 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.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)) rw@1.3.3: {} @@ -25153,7 +25083,7 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sass@1.101.0: + sass@1.102.0: dependencies: chokidar: 5.0.0 immutable: 5.1.6 @@ -25211,9 +25141,9 @@ snapshots: semver@7.8.5: {} - send@0.19.2: + send@0.19.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -25229,9 +25159,9 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1: + send@1.2.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -25259,11 +25189,11 @@ snapshots: path-to-regexp: 3.3.0 range-parser: 1.2.0 - serve-index@1.9.2: + serve-index@1.9.2(supports-color@8.1.1): dependencies: accepts: 1.3.8 batch: 0.6.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) escape-html: 1.0.3 http-errors: 1.8.1 mime-types: 2.1.35 @@ -25271,21 +25201,21 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.16.3: + serve-static@1.16.3(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color - serve-static@2.2.1: + serve-static@2.2.1(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25404,7 +25334,7 @@ snapshots: simple-concat: 1.0.1 optional: true - simple-icons@16.26.0: {} + simple-icons@16.27.1: {} sirv@2.0.4: dependencies: @@ -25442,42 +25372,42 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-adapter@2.5.7: + socket.io-adapter@2.5.7(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) ws: 8.20.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-client@4.8.3: + socket.io-client@4.8.3(supports-color@8.1.1): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 - engine.io-client: 6.6.5 - socket.io-parser: 4.2.6 + debug: 4.4.3(supports-color@8.1.1) + engine.io-client: 6.6.5(supports-color@8.1.1) + socket.io-parser: 4.2.6(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-parser@4.2.6: + socket.io-parser@4.2.6(supports-color@8.1.1): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - socket.io@4.8.3: + socket.io@4.8.3(supports-color@8.1.1): dependencies: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.6 - debug: 4.4.3 - engine.io: 6.6.8 - socket.io-adapter: 2.5.7 - socket.io-parser: 4.2.6 + debug: 4.4.3(supports-color@8.1.1) + engine.io: 6.6.8(supports-color@8.1.1) + socket.io-adapter: 2.5.7(supports-color@8.1.1) + socket.io-parser: 4.2.6(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color @@ -25508,9 +25438,9 @@ snapshots: space-separated-tokens@2.0.2: {} - spdy-transport@3.0.0: + spdy-transport@3.0.0(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -25519,13 +25449,13 @@ snapshots: transitivePeerDependencies: - supports-color - spdy@4.0.2: + spdy@4.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 - spdy-transport: 3.0.0 + spdy-transport: 3.0.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25665,10 +25595,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylehacks@6.1.1(postcss@8.5.19): + stylehacks@6.1.1(postcss@8.5.25): dependencies: browserslist: 4.28.6 - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 stylis@4.4.0: {} @@ -25689,11 +25619,11 @@ snapshots: make-asynchronous: 1.1.0 time-span: 5.1.0 - superagent@10.3.0: + superagent@10.3.0(supports-color@8.1.1): dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.6 formidable: 3.5.4 @@ -25703,11 +25633,11 @@ snapshots: transitivePeerDependencies: - supports-color - supertest@7.2.2: + supertest@7.2.2(supports-color@8.1.1): dependencies: cookie-signature: 1.2.2 methods: 1.1.2 - superagent: 10.3.0 + superagent: 10.3.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25721,11 +25651,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome@3.3.5(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-awesome@3.3.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-check@4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.5(@typescript-eslint/types@8.64.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)): dependencies: '@jridgewell/trace-mapping': 0.3.31 '@sveltejs/load-config': 0.2.0 @@ -25733,27 +25663,27 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.8.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - postcss: 8.5.19 - postcss-scss: 4.0.9(postcss@8.5.19) + postcss: 8.5.25 + postcss-scss: 4.0.9(postcss@8.5.25) postcss-selector-parser: 7.1.4 semver: 7.8.5 optionalDependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) svelte-floating-ui@1.5.8: dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/dom': 1.7.6 + '@floating-ui/core': 1.8.0 + '@floating-ui/dom': 1.8.0 svelte-gestures@5.2.2: {} @@ -25761,7 +25691,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-i18n@4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -25769,85 +25699,85 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) tiny-glob: 0.2.9 - svelte-jsoneditor@3.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-jsoneditor@3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/commands': 6.10.3 + '@codemirror/commands': 6.10.4 '@codemirror/lang-json': 6.0.2 - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@codemirror/lint': 6.9.7 - '@codemirror/search': 6.7.0 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 - '@fortawesome/free-regular-svg-icons': 7.2.0 - '@fortawesome/free-solid-svg-icons': 7.2.0 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + '@fortawesome/free-regular-svg-icons': 7.3.1 + '@fortawesome/free-solid-svg-icons': 7.3.1 '@jsonquerylang/jsonquery': 5.1.1 '@lezer/highlight': 1.2.3 - '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1) + '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7) ajv: 8.20.0 - codemirror-wrapped-line-indent: 1.0.9(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1) + codemirror-wrapped-line-indent: 1.0.9(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7) diff-sequences: 29.6.3 - immutable-json-patch: 6.0.2 + immutable-json-patch: 6.0.3 jmespath: 0.16.0 json-source-map: 0.6.1 jsonpath-plus: 10.4.0 - jsonrepair: 3.14.0 + jsonrepair: 3.15.0 lodash-es: 4.18.1 memoize-one: 6.0.0 natural-compare-lite: 1.4.0 - sass: 1.101.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - svelte-awesome: 3.3.5(svelte@5.56.5(@typescript-eslint/types@8.64.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-select: 5.8.3 vanilla-picker: 2.12.3 - svelte-maplibre@1.3.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-maplibre@1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.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.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-parse-markup@0.1.5(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-parse-markup@0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-persisted-store@0.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-persisted-store@0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.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)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + 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)) style-to-object: 1.0.14 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.56.5(@typescript-eslint/types@8.64.0): + svelte@5.56.8(@typescript-eslint/types@8.65.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 - acorn: 8.17.0 + acorn: 8.18.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.1 + devalue: 5.8.2 esm-env: 1.2.2 - esrap: 2.2.13(@typescript-eslint/types@8.64.0) + esrap: 2.2.13(@typescript-eslint/types@8.65.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -25890,9 +25820,9 @@ snapshots: tailwind-merge@3.6.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.2): + tailwind-variants@3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3): dependencies: - tailwindcss: 4.3.2 + tailwindcss: 4.3.3 optionalDependencies: tailwind-merge: 3.6.0 @@ -25926,11 +25856,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.19 - postcss-import: 15.1.0(postcss@8.5.19) - postcss-js: 4.1.0(postcss@8.5.19) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.19)(tsx@4.23.1)(yaml@2.9.0) - postcss-nested: 6.2.0(postcss@8.5.19) + 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-selector-parser: 6.1.4 resolve: 1.22.12 sucrase: 3.35.1 @@ -25938,7 +25868,7 @@ snapshots: - tsx - yaml - tailwindcss@4.3.2: {} + tailwindcss@4.3.3: {} tapable@2.3.3: {} @@ -26004,51 +25934,62 @@ snapshots: - bare-abort-controller - react-native-b4a - terser-webpack-plugin@5.6.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)): + 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)): 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.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0) - optionalDependencies: - '@swc/core': 1.15.43(@swc/helpers@0.5.23) - esbuild: 0.28.1 - lightningcss: 1.33.0 - - terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): - 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(postcss@8.5.19) + 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) optionalDependencies: + '@swc/core': 1.15.46(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.19) + cssnano: 6.1.2(postcss@8.5.25) + csso: 5.0.5 + esbuild: 0.28.1 html-minifier-terser: 7.2.0 - postcss: 8.5.19 + lightningcss: 1.33.0 + postcss: 8.5.25 + 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)): + 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) + optionalDependencies: + '@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@5.49.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.17.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 - testcontainers@12.0.4: + testcontainers@12.0.4(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 4.0.1 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) docker-compose: 1.4.2 - dockerode: 5.0.1 + dockerode: 5.0.1(supports-color@8.1.1) get-port: 5.1.1 proper-lockfile: 4.1.2 - properties-reader: 3.0.1 + properties-reader: 3.0.1(supports-color@8.1.1) ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.3 tmp: 0.2.7 @@ -26079,8 +26020,6 @@ snapshots: dependencies: tslib: 2.8.1 - three@0.184.0: {} - three@0.185.1: {} through@2.3.8: {} @@ -26210,7 +26149,7 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 tapable: 2.3.3 tsconfig-paths: 4.2.0 @@ -26278,13 +26217,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)): + 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): dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - eslint: 10.7.0(jiti@2.7.0) + '@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) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -26432,10 +26371,10 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.43(@swc/helpers@0.5.23))(rollup@4.62.0): + unplugin-swc@1.5.9(@swc/core@1.15.46(@swc/helpers@0.5.23))(rollup@4.62.0): dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.62.0) - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@swc/core': 1.15.46(@swc/helpers@0.5.23) load-tsconfig: 0.2.5 unplugin: 2.3.11 transitivePeerDependencies: @@ -26444,7 +26383,7 @@ snapshots: unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.17.0 + acorn: 8.18.0 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 @@ -26477,14 +26416,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + 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) optionalDependencies: - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) + 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)) url@0.11.4: dependencies: @@ -26503,9 +26442,9 @@ snapshots: utils-merge@1.0.1: {} - utimes@5.2.1: + utimes@5.2.1(supports-color@8.1.1): dependencies: - '@mapbox/node-pre-gyp': 1.0.11 + '@mapbox/node-pre-gyp': 1.0.11(supports-color@8.1.1) node-addon-api: 4.3.0 transitivePeerDependencies: - encoding @@ -26566,13 +26505,13 @@ snapshots: transitivePeerDependencies: - rollup - vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.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): dependencies: cac: 6.7.14 - debug: 4.4.3 + 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.101.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.1)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -26587,22 +26526,22 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.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.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)): dependencies: - debug: 4.4.3 + 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.101.0)(terser@5.49.0)(tsx@4.23.1)(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) transitivePeerDependencies: - supports-color - typescript - vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.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.1)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - postcss: 8.5.19 + postcss: 8.5.25 rollup: 4.62.0 tinyglobby: 0.2.17 optionalDependencies: @@ -26610,16 +26549,16 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.33.0 - sass: 1.101.0 + sass: 1.102.0 terser: 5.49.0 tsx: 4.23.1 yaml: 2.9.0 - vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(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): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.19 + postcss: 8.5.25 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: @@ -26627,31 +26566,31 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 - sass: 1.101.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.101.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)): optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(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) 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.10.6)(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.101.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)) - vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(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.1)(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.101.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.1)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 '@vitest/spy': 3.2.7 '@vitest/utils': 3.2.7 chai: 5.3.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) expect-type: 1.4.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -26662,14 +26601,14 @@ 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.101.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.101.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.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) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 '@types/node': 24.13.3 - happy-dom: 20.10.6 - jsdom: 26.1.0(canvas@3.2.3) + happy-dom: 20.11.1 + jsdom: 26.1.0(canvas@3.2.3)(supports-color@8.1.1) transitivePeerDependencies: - jiti - less @@ -26684,10 +26623,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.10.6)(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.101.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)): 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.101.0)(terser@5.49.0)(tsx@4.23.1)(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/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -26704,13 +26643,44 @@ 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.101.0)(terser@5.49.0)(tsx@4.23.1)(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) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.13.3 '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) - happy-dom: 20.10.6 + happy-dom: 20.11.1 + jsdom: 26.1.0(canvas@3.2.3)(supports-color@8.1.1) + 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)): + 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/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + 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) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 24.13.3 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + happy-dom: 20.11.1 jsdom: 26.1.0(canvas@3.2.3) transitivePeerDependencies: - msw @@ -26748,7 +26718,7 @@ snapshots: webpack-bundle-analyzer@4.10.2: dependencies: '@discoveryjs/json-ext': 0.5.7 - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk: 8.3.5 commander: 7.2.0 debounce: 1.2.1 @@ -26763,7 +26733,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: colorette: 2.0.20 memfs: 4.64.0(tslib@2.8.1) @@ -26772,11 +26742,11 @@ snapshots: range-parser: 1.3.0 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - tslib - webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -26790,24 +26760,24 @@ snapshots: bonjour-service: 1.4.3 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.8.1 + compression: 1.8.1(supports-color@8.1.1) connect-history-api-fallback: 2.0.0 - express: 4.22.2 + express: 4.22.2(supports-color@8.1.1) graceful-fs: 4.2.11 - http-proxy-middleware: 2.0.10(@types/express@4.17.25) + http-proxy-middleware: 2.0.10(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)) ipaddr.js: 2.4.0 launch-editor: 2.14.1 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 selfsigned: 5.5.0 - serve-index: 1.9.2 + serve-index: 1.9.2(supports-color@8.1.1) sockjs: 0.3.24 - spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)) + 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)) ws: 8.21.1 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + 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) transitivePeerDependencies: - bufferutil - debug @@ -26833,7 +26803,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.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): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -26841,11 +26811,11 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.6 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 @@ -26856,7 +26826,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.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)) + 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)) watchpack: 2.5.2 webpack-sources: 3.5.1 transitivePeerDependencies: @@ -26873,25 +26843,25 @@ snapshots: - postcss - uglify-js - webpack@5.108.4(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19): + 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): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.6 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 graceful-fs: 4.2.11 loader-runner: 4.3.2 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) + 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)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 @@ -26911,52 +26881,14 @@ snapshots: - postcss - uglify-js - webpack@5.108.4(postcss@8.5.19): - dependencies: - '@types/estree': 1.0.9 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.6 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.2 - es-module-lexer: 2.3.1 - eslint-scope: 5.1.1 - events: 3.3.0 - graceful-fs: 4.2.11 - loader-runner: 4.3.2 - mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.3 - watchpack: 2.5.2 - webpack-sources: 3.5.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - uglify-js - - webpackbar@7.0.0(webpack@5.108.4(postcss@8.5.19)): + 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)): dependencies: ansis: 3.17.0 consola: 3.4.2 pretty-time: 1.1.0 std-env: 3.10.0 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + 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) websocket-driver@0.7.5: dependencies: diff --git a/server/Dockerfile b/server/Dockerfile index b7a4e105a3..802877421d 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -56,16 +56,17 @@ FROM builder AS plugins ARG TARGETPLATFORM -COPY --from=ghcr.io/jdx/mise:2026.7.15@sha256:e62097049bfc980de5d9a25fbe16431e24c1e7713b3f77b18c04f81c25239953 /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.8.1@sha256:b2297770273f71e685b8056e3b07bfda4ffc35f0fb62e0339b3cdc1e5766e2fe /usr/local/bin/mise /usr/local/bin/mise WORKDIR /app COPY ./mise.toml ./mise.toml +COPY ./mise.lock ./mise.lock COPY ./packages/plugin-core/mise.toml ./packages/plugin-core/ ENV MISE_TRUSTED_CONFIG_PATHS=/app/mise.toml ENV MISE_DATA_DIR=/buildcache/mise ENV MISE_DISABLE_TOOLS=flutter RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ - mise install + mise install --locked COPY ./packages/sdk ./packages/sdk/ COPY ./packages/plugin-core ./packages/plugin-core/ diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 178f4440df..23a565b4bb 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.7.15@sha256:e62097049bfc980de5d9a25fbe16431e24c1e7713b3f77b18c04f81c25239953 /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.8.1@sha256:b2297770273f71e685b8056e3b07bfda4ffc35f0fb62e0339b3cdc1e5766e2fe /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/package.json b/server/package.json index 048ea06040..7d5f73cc61 100644 --- a/server/package.json +++ b/server/package.json @@ -49,14 +49,14 @@ "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.220.0", - "@opentelemetry/instrumentation-http": "^0.220.0", - "@opentelemetry/instrumentation-ioredis": "^0.68.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.66.0", - "@opentelemetry/instrumentation-pg": "^0.72.0", + "@opentelemetry/exporter-prometheus": "^0.221.0", + "@opentelemetry/instrumentation-http": "^0.221.0", + "@opentelemetry/instrumentation-ioredis": "^0.69.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.67.0", + "@opentelemetry/instrumentation-pg": "^0.73.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.220.0", + "@opentelemetry/sdk-node": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^1.0.0", "@react-email/render": "^2.0.0", @@ -82,7 +82,7 @@ "i18n-iso-countries": "^7.6.0", "ioredis": "^5.8.2", "jose": "^6.0.0", - "js-yaml": "^4.1.0", + "js-yaml": "^5.0.0", "jsonwebtoken": "^9.0.2", "kysely": "0.28.17", "kysely-postgres-js": "^3.0.0", diff --git a/server/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts index 3fe9b49368..eca7e5bcb5 100644 --- a/server/src/controllers/app.controller.ts +++ b/server/src/controllers/app.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Header } from '@nestjs/common'; import { ApiExcludeEndpoint } from '@nestjs/swagger'; +import { Authenticated } from 'src/middleware/auth.guard'; import { SystemConfigService } from 'src/services/system-config.service'; @Controller() @@ -8,6 +9,7 @@ export class AppController { @ApiExcludeEndpoint() @Get('.well-known/immich') + @Authenticated({ public: true }) getImmichWellKnown() { return { api: { @@ -18,6 +20,7 @@ export class AppController { @ApiExcludeEndpoint() @Get('custom.css') + @Authenticated({ public: true }) @Header('Content-Type', 'text/css') getCustomCss() { return this.service.getCustomCss(); diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts index d105dd90b9..3bf1d59f8b 100644 --- a/server/src/controllers/auth.controller.spec.ts +++ b/server/src/controllers/auth.controller.spec.ts @@ -1,3 +1,4 @@ +import { BadRequestException } from '@nestjs/common'; import { AuthController } from 'src/controllers/auth.controller'; import { LoginResponseDto } from 'src/dtos/auth.dto'; import { AuthService } from 'src/services/auth.service'; @@ -76,6 +77,18 @@ describe(AuthController.name, () => { .send({ name: 'admin', password: 'password', email: 'admin@local' }); expect(status).toEqual(201); }); + + it('should not sign up an admin when setup is unavailable', async () => { + ctx.requireSetupAvailable.mockRejectedValue(new BadRequestException('Admin setup is not available')); + + const { status, body } = await request(ctx.getHttpServer()) + .post('/auth/admin-sign-up') + .send({ name, email, password }); + + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest('Admin setup is not available')); + expect(service.adminSignUp).not.toHaveBeenCalled(); + }); }); describe('POST /auth/login', () => { diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 63cdce4f32..b96ba4dee3 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -33,6 +33,7 @@ export class AuthController { description: 'Login with username and password and receive a session token.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) + @Authenticated({ public: true }) async login( @Res({ passthrough: true }) res: Response, @Body() loginCredential: LoginCredentialDto, @@ -55,6 +56,7 @@ export class AuthController { description: 'Create the first admin user in the system.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) + @Authenticated({ public: true, setup: true }) signUpAdmin(@Body() dto: SignUpDto): Promise { return this.service.adminSignUp(dto); } diff --git a/server/src/controllers/database-backup.controller.spec.ts b/server/src/controllers/database-backup.controller.spec.ts new file mode 100644 index 0000000000..43fa779e8d --- /dev/null +++ b/server/src/controllers/database-backup.controller.spec.ts @@ -0,0 +1,55 @@ +import { BadRequestException } from '@nestjs/common'; +import { DatabaseBackupController } from 'src/controllers/database-backup.controller'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { MaintenanceService } from 'src/services/maintenance.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(DatabaseBackupController.name, () => { + let ctx: ControllerContext; + const service = automock(DatabaseBackupService, { args: [{ setContext: () => {} }], strict: false }); + const maintenanceService = mockBaseService(MaintenanceService); + + beforeAll(async () => { + ctx = await controllerSetup(DatabaseBackupController, [ + { provide: DatabaseBackupService, useValue: service }, + { provide: MaintenanceService, useValue: maintenanceService }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + maintenanceService.resetAllMocks(); + 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' }); + + await request(ctx.getHttpServer()).post('/admin/database-backups/start-restore').send(); + + expect(ctx.authenticate).not.toHaveBeenCalled(); + expect(ctx.requireSetupAvailable).toHaveBeenCalled(); + }); + + it('should not start a restore when setup is unavailable', async () => { + ctx.requireSetupAvailable.mockRejectedValue(new BadRequestException('Admin setup is not available')); + + const { status, body } = await request(ctx.getHttpServer()).post('/admin/database-backups/start-restore').send(); + + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest('Admin setup is not available')); + expect(maintenanceService.startRestoreFlow).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/database-backup.controller.ts b/server/src/controllers/database-backup.controller.ts index 737c8f3958..4cb2092d1b 100644 --- a/server/src/controllers/database-backup.controller.ts +++ b/server/src/controllers/database-backup.controller.ts @@ -71,6 +71,7 @@ export class DatabaseBackupController { description: 'Put Immich into maintenance mode to restore a backup (Immich must not be configured)', history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), }) + @Authenticated({ public: true, setup: true }) async startDatabaseRestoreFlow( @GetLoginDetails() loginDetails: LoginDetails, @Res({ passthrough: true }) res: Response, diff --git a/server/src/controllers/index.spec.ts b/server/src/controllers/index.spec.ts index 67962e8b3c..3d39a4dd3d 100644 --- a/server/src/controllers/index.spec.ts +++ b/server/src/controllers/index.spec.ts @@ -53,12 +53,10 @@ describe('controllers', () => { expect(new Set(reachableByNonAdmins)).toEqual(UNAUTHENTICATED_ADMIN_ROUTES); }); - it('should not authenticate the bootstrap routes under admin/', () => { - const authenticated = routes - .filter((route) => UNAUTHENTICATED_ADMIN_ROUTES.has(route.id) && route.auth !== undefined) - .map((route) => route.label); + it('should declare authentication on every route', () => { + const undeclared = routes.filter((route) => route.auth === undefined).map((route) => route.label); - expect(authenticated).toEqual([]); + expect(undeclared).toEqual([]); }); it('should require admin access for routes with an admin permission', () => { diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index d5f13e341c..3d8c6c1194 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -27,6 +27,7 @@ export class MaintenanceController { description: 'Fetch information about the currently running maintenance action.', history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), }) + @Authenticated({ public: true }) getMaintenanceStatus(): MaintenanceStatusResponseDto { return this.service.getMaintenanceStatus(); } @@ -48,6 +49,7 @@ export class MaintenanceController { description: 'Login with maintenance token or cookie to receive current information and perform further actions.', history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'), }) + @Authenticated({ public: true }) maintenanceLogin(@Body() _dto: MaintenanceLoginDto): MaintenanceAuthDto { throw new BadRequestException('Not in maintenance mode'); } diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index 7f2313a058..54f5c1f10b 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -22,6 +22,7 @@ export class OAuthController { constructor(private service: AuthService) {} @Get('mobile-redirect') + @Authenticated({ public: true }) @Redirect() @Endpoint({ summary: 'Redirect OAuth to mobile', @@ -37,6 +38,7 @@ export class OAuthController { } @Post('authorize') + @Authenticated({ public: true }) @Endpoint({ summary: 'Start OAuth', description: 'Initiate the OAuth authorization process.', @@ -62,6 +64,7 @@ export class OAuthController { } @Post('callback') + @Authenticated({ public: true }) @Endpoint({ summary: 'Finish OAuth', description: 'Complete the OAuth authorization process by exchanging the authorization code for a session token.', @@ -115,6 +118,7 @@ export class OAuthController { } @Post('backchannel-logout') + @Authenticated({ public: true }) @HttpCode(HttpStatus.OK) @ApiConsumes('application/x-www-form-urlencoded') @Endpoint({ diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index f5ce4b851c..6407155492 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -64,6 +64,7 @@ export class ServerController { } @Get('ping') + @Authenticated({ public: true }) @Endpoint({ summary: 'Ping', description: 'Pong', @@ -74,6 +75,7 @@ export class ServerController { } @Get('version') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get server version', description: 'Retrieve the current server version in semantic versioning (semver) format.', @@ -84,6 +86,7 @@ export class ServerController { } @Get('version-history') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get version history', description: 'Retrieve a list of past versions the server has been on.', @@ -94,6 +97,7 @@ export class ServerController { } @Get('features') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get features', description: 'Retrieve available features supported by this server.', @@ -104,6 +108,7 @@ export class ServerController { } @Get('config') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get config', description: 'Retrieve the current server configuration.', @@ -125,6 +130,7 @@ export class ServerController { } @Get('media-types') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get supported media types', description: 'Retrieve all media types supported by the server.', diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 3c871f672d..1e3b3b6193 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -34,7 +34,22 @@ const AlbumUserCreateSchema = z const CreateAlbumSchema = z .object({ albumName: z.string().describe('Album name'), - description: z.string().optional().describe('Album description'), + // TODO: drop the empty-string-to-null transform in v4 (clients should send null) + description: z + .string() + .nullable() + .transform((value) => (value === '' ? null : value)) + .optional() + .describe('Album description') + .meta({ + ...new HistoryBuilder() + .added('v1') + .updated( + 'v3', + 'Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4.', + ) + .getExtensions(), + }), albumUsers: z.array(AlbumUserCreateSchema).optional().describe('Album users'), assetIds: z.array(z.uuidv4()).optional().describe('Initial asset IDs'), }) @@ -57,7 +72,22 @@ const AlbumsAddAssetsResponseSchema = z const UpdateAlbumSchema = z .object({ albumName: z.string().optional().describe('Album name'), - description: z.string().optional().describe('Album description'), + // TODO: drop the empty-string-to-null transform in v4 (clients should send null) + description: z + .string() + .nullable() + .transform((value) => (value === '' ? null : value)) + .optional() + .describe('Album description') + .meta({ + ...new HistoryBuilder() + .added('v1') + .updated( + 'v3', + 'Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4.', + ) + .getExtensions(), + }), albumThumbnailAssetId: z.uuidv4().optional().describe('Album thumbnail asset ID'), isActivityEnabled: z.boolean().optional().describe('Enable activity feed'), order: AssetOrderSchema.optional(), @@ -110,7 +140,18 @@ export const AlbumResponseSchema = z .object({ id: z.uuidv4().describe('Album ID'), albumName: z.string().describe('Album name'), - description: z.string().describe('Album description'), + description: z + .string() + .describe('Album description') + .meta({ + ...new HistoryBuilder() + .added('v1') + .updated( + 'v3', + 'An empty string is returned instead of null for backwards compatibility; null will be returned in v4.', + ) + .getExtensions(), + }), // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. @@ -171,7 +212,7 @@ export type MapAlbumDto = { assets?: ShallowDehydrateObject[]; sharedLinks?: ShallowDehydrateObject[]; albumName: string; - description: string; + description: string | null; albumThumbnailAssetId: string | null; createdAt: Date; updatedAt: Date; @@ -207,7 +248,8 @@ export const mapAlbum = (entity: MaybeDehydrated): AlbumResponseDto return { albumName: entity.albumName, - description: entity.description, + // TODO: return null instead of '' in v4 + description: entity.description ?? '', albumThumbnailAssetId: entity.albumThumbnailAssetId, createdAt: asDateTimeString(entity.createdAt), updatedAt: asDateTimeString(entity.updatedAt), diff --git a/server/src/middleware/auth.guard.spec.ts b/server/src/middleware/auth.guard.spec.ts new file mode 100644 index 0000000000..5b094c03c1 --- /dev/null +++ b/server/src/middleware/auth.guard.spec.ts @@ -0,0 +1,82 @@ +import { ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Authenticated, AuthGuard } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { AuthService } from 'src/services/auth.service'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { newTestService, ServiceMocks } from 'test/utils'; + +class TestController { + @Authenticated({ public: true, setup: true }) + setupRoute() {} + + @Authenticated({ public: true }) + publicRoute() {} + + undecoratedRoute() {} +} + +const contextFor = (handler: () => void) => + ({ + getHandler: () => handler, + switchToHttp: () => ({ getRequest: () => ({ headers: {}, query: {}, path: '/' }) }), + }) as unknown as ExecutionContext; + +describe(AuthGuard.name, () => { + let sut: AuthGuard; + let authService: AuthService; + let mocks: ServiceMocks; + + beforeEach(() => { + ({ sut: authService, mocks } = newTestService(AuthService)); + sut = new AuthGuard(mocks.logger as unknown as LoggingRepository, new Reflector(), authService); + }); + + describe('setup routes', () => { + it('should allow access while the server is awaiting its first admin', async () => { + mocks.user.hasAdmin.mockResolvedValue(false); + const authenticate = vitest.spyOn(authService, 'authenticate'); + + await expect(sut.canActivate(contextFor(TestController.prototype.setupRoute))).resolves.toBe(true); + + expect(authenticate).not.toHaveBeenCalled(); + }); + + it('should reject when setup is disabled', async () => { + mocks.config.getEnv.mockReturnValue(mockEnvData({ setup: { allow: false } })); + mocks.user.hasAdmin.mockResolvedValue(false); + + await expect(sut.canActivate(contextFor(TestController.prototype.setupRoute))).rejects.toThrowError( + 'Admin setup is not available', + ); + }); + + it('should reject when the server already has an admin', async () => { + mocks.user.hasAdmin.mockResolvedValue(true); + + await expect(sut.canActivate(contextFor(TestController.prototype.setupRoute))).rejects.toThrowError( + 'Admin setup is not available', + ); + }); + }); + + describe('public routes', () => { + it('should not require setup availability', async () => { + mocks.user.hasAdmin.mockResolvedValue(true); + + await expect(sut.canActivate(contextFor(TestController.prototype.publicRoute))).resolves.toBe(true); + }); + }); + + describe('undecorated routes', () => { + it('should be rejected', async () => { + const authenticate = vitest.spyOn(authService, 'authenticate'); + + await expect(sut.canActivate(contextFor(TestController.prototype.undecoratedRoute))).rejects.toThrowError( + 'does not declare @Authenticated()', + ); + + expect(authenticate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index d9870ec7b9..93bcfe26e7 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -17,23 +17,26 @@ import { getUserAgentDetails } from 'src/utils/request'; type AdminRoute = { admin?: true }; type SharedLinkRoute = { sharedLink?: true }; -export type AuthenticatedOptions = { permission?: Permission | false } & (AdminRoute | SharedLinkRoute); +type AuthorizedRoute = { permission?: Permission | false; public?: never; setup?: never } & ( + AdminRoute | SharedLinkRoute +); +type PublicRoute = { public: true; setup?: true; permission?: never; admin?: never; sharedLink?: never }; +export type AuthenticatedOptions = AuthorizedRoute | PublicRoute; type ReflectorTarget = Parameters[1]; /** Resolves the `@Authenticated()` options of a route handler, with the defaults applied. */ export const getAuthenticatedOptions = (reflector: Reflector, target: ReflectorTarget) => { const options = reflector.getAllAndOverride(MetadataKey.AuthRoute, [target]); - return options && { sharedLink: false, admin: false, ...options }; + return options && { sharedLink: false, admin: false, public: false, setup: false, ...options }; }; export const Authenticated = (options: AuthenticatedOptions = {}): MethodDecorator => { - const decorators: MethodDecorator[] = [ - ApiBearerAuth(), - ApiCookieAuth(), - ApiSecurity(MetadataKey.ApiKeySecurity), - SetMetadata(MetadataKey.AuthRoute, options), - ]; + const decorators: MethodDecorator[] = [SetMetadata(MetadataKey.AuthRoute, options)]; + + if (!options.public) { + decorators.push(ApiBearerAuth(), ApiCookieAuth(), ApiSecurity(MetadataKey.ApiKeySecurity)); + } if ((options as AdminRoute).admin) { decorators.push(ApiExtension(ApiCustomExtension.AdminOnly, true)); @@ -96,6 +99,14 @@ export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const options = getAuthenticatedOptions(this.reflector, context.getHandler()); if (!options) { + throw new Error(`Route ${context.getHandler().name} does not declare @Authenticated()`); + } + + if (options.setup) { + await this.authService.requireSetupAvailable(); + } + + if (options.public) { return true; } diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 3fc9fea2e3..1b5592d356 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -9,6 +9,7 @@ import semver from 'semver'; import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, + serverVersion, VECTOR_EXTENSIONS, VECTOR_INDEX_TABLES, VECTOR_VERSION_RANGE, @@ -382,6 +383,17 @@ export class DatabaseRepository { if (error) { this.logger.error(`Migrations failed: ${error}`); + + const missing = + error instanceof Error ? error.message.match(/previously executed migration (.+) is missing/u) : null; + if (missing) { + throw new Error( + `Migration "${missing[1]}" was already applied to this database but is not in this version of Immich (${serverVersion}). ` + + `This usually means the database was migrated by a newer version. Downgrades are not supported.`, + { cause: error }, + ); + } + throw error; } diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 94047bf326..1d504f6c71 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -20,7 +20,8 @@ type TagsWithWrongTypes = | 'TagsList' | 'Keywords' | 'HierarchicalSubject' - | 'ISO'; + | 'ISO' + | 'LensModel'; export interface ImmichTags extends Omit { ContentIdentifier?: string; @@ -43,6 +44,9 @@ export interface ImmichTags extends Omit { Description?: StringOrNumber; ImageDescription?: StringOrNumber; + // Apparently LensModel can also be a float: https://github.com/immich-app/immich/issues/30492 + LensModel?: StringOrNumber; + // Extended properties for image regions, such as faces RegionInfo?: { AppliedToDimensions: { diff --git a/server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts b/server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts new file mode 100644 index 0000000000..82f742c90a --- /dev/null +++ b/server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts @@ -0,0 +1,13 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "user" ALTER COLUMN "password" DROP NOT NULL;`.execute(db); + await sql`ALTER TABLE "user" ALTER COLUMN "password" SET DEFAULT NULL;`.execute(db); + await sql`UPDATE "user" SET "password" = NULL WHERE "password" = '';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`UPDATE "user" SET "password" = '' WHERE "password" IS NULL;`.execute(db); + await sql`ALTER TABLE "user" ALTER COLUMN "password" SET DEFAULT '';`.execute(db); + await sql`ALTER TABLE "user" ALTER COLUMN "password" SET NOT NULL;`.execute(db); +} diff --git a/server/src/schema/migrations/1784986754474-AlbumDescriptionNullable.ts b/server/src/schema/migrations/1784986754474-AlbumDescriptionNullable.ts new file mode 100644 index 0000000000..3be799f498 --- /dev/null +++ b/server/src/schema/migrations/1784986754474-AlbumDescriptionNullable.ts @@ -0,0 +1,13 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "album" ALTER COLUMN "description" DROP NOT NULL;`.execute(db); + await sql`ALTER TABLE "album" ALTER COLUMN "description" SET DEFAULT NULL;`.execute(db); + await sql`UPDATE "album" SET "description" = NULL WHERE "description" = '';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`UPDATE "album" SET "description" = '' WHERE "description" IS NULL;`.execute(db); + await sql`ALTER TABLE "album" ALTER COLUMN "description" SET DEFAULT ''::text;`.execute(db); + await sql`ALTER TABLE "album" ALTER COLUMN "description" SET NOT NULL;`.execute(db); +} diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index f54658be65..c0d13d4902 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -36,8 +36,8 @@ export class AlbumTable { @UpdateDateColumn() updatedAt!: Generated; - @Column({ type: 'text', default: '' }) - description!: Generated; + @Column({ type: 'text', nullable: true }) + description!: string | null; @DeleteDateColumn() deletedAt!: Timestamp | null; diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 0839924d2a..50d56d9067 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -31,8 +31,8 @@ export class UserTable { @Column({ unique: true }) email!: string; - @Column({ default: '' }) - password!: Generated; + @Column({ nullable: true, default: null }) + password!: string | null; @Column({ nullable: true }) pinCode!: string | null; diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 5a9a85f179..14527debba 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -317,6 +317,12 @@ describe(AssetMediaService.name, () => { ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.create).not.toHaveBeenCalled(); + expect(mocks.asset.remove).not.toHaveBeenCalled(); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: [file.originalPath, undefined] }, + }); + expect(mocks.event.emit).not.toHaveBeenCalled(); expect(mocks.user.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size); expect(mocks.storage.utimes).not.toHaveBeenCalledWith( file.originalPath, diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 818bd5eb91..1bb9d1e5b6 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; -import { AuthSharedLink } from 'src/database'; +import { Asset, AuthSharedLink } from 'src/database'; import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, @@ -128,6 +128,7 @@ export class AssetMediaService extends BaseService { file: UploadFile, sidecarFile?: UploadFile, ): Promise { + let asset: Asset | undefined; try { await this.requireAccess({ auth, @@ -145,7 +146,7 @@ export class AssetMediaService extends BaseService { ); } - const asset = await this.assetRepository.create({ + asset = await this.assetRepository.create({ ownerId: auth.user.id, libraryId: null, @@ -215,6 +216,11 @@ export class AssetMediaService extends BaseService { return { status: AssetMediaStatus.DUPLICATE, id: duplicateId }; } + // clean up the asset row if one was created + if (asset) { + await this.assetRepository.remove({ id: asset.id }); + } + this.logger.error(`Error uploading file ${error}`, error?.stack); throw error; } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index dad13ef5f5..48dcf5a509 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -307,16 +307,7 @@ describe(AuthService.name, () => { describe('adminSignUp', () => { const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' }; - it('should only allow one admin', async () => { - mocks.user.getAdmin.mockResolvedValue({} as UserAdmin); - - await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException); - - expect(mocks.user.getAdmin).toHaveBeenCalled(); - }); - it('should sign up the admin', async () => { - mocks.user.getAdmin.mockResolvedValue(void 0); mocks.user.create.mockResolvedValue({ ...userStub.admin, ...dto, @@ -334,7 +325,6 @@ describe(AuthService.name, () => { name: 'immich admin', }); - expect(mocks.user.getAdmin).toHaveBeenCalled(); expect(mocks.user.create).toHaveBeenCalled(); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index f3be40f7dd..59e20276af 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -197,16 +197,6 @@ export class AuthService extends BaseService { } async adminSignUp(dto: SignUpDto): Promise { - const { setup } = this.configRepository.getEnv(); - if (!setup.allow) { - throw new BadRequestException('Admin setup is disabled'); - } - - const adminUser = await this.userRepository.getAdmin(); - if (adminUser) { - throw new BadRequestException('The server already has an admin'); - } - const admin = await this.createUser({ isAdmin: true, email: dto.email, diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 6d17410441..2195c66a22 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -280,6 +280,17 @@ export class BaseService { return checkAccess(this.accessRepository, request); } + async isSetupAvailable(): Promise { + const { setup } = this.configRepository.getEnv(); + return setup.allow && !(await this.userRepository.hasAdmin()); + } + + async requireSetupAvailable(): Promise { + if (!(await this.isSetupAvailable())) { + throw new BadRequestException('Admin setup is not available'); + } + } + async createUser(dto: Insertable & { email: string }): Promise { const exists = await this.userRepository.getByEmail(dto.email); if (exists) { diff --git a/server/src/services/maintenance.service.spec.ts b/server/src/services/maintenance.service.spec.ts index e598f1c71d..1eaefb689d 100644 --- a/server/src/services/maintenance.service.spec.ts +++ b/server/src/services/maintenance.service.spec.ts @@ -134,6 +134,22 @@ describe(MaintenanceService.name, () => { }); }); + describe('startRestoreFlow', () => { + it('should start maintenance mode and return a jwt', async () => { + mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); + + await expect(sut.startRestoreFlow()).resolves.toMatchObject({ jwt: expect.any(String) }); + + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret: expect.stringMatching(/^\w{128}$/), + action: { + action: MaintenanceAction.SelectDatabaseRestore, + }, + }); + }); + }); + describe('createLoginUrl', () => { it('should fail outside of maintenance mode without secret', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index ca1e05d93f..bcd6f2e834 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; import { MaintenanceAuthDto, @@ -59,11 +59,6 @@ export class MaintenanceService extends BaseService { } async startRestoreFlow(): Promise<{ jwt: string }> { - const adminUser = await this.userRepository.getAdmin(); - if (adminUser) { - throw new BadRequestException('The server already has an admin'); - } - return this.startMaintenance( { action: MaintenanceAction.SelectDatabaseRestore, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 171dcfe514..37dd92e27d 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -117,7 +117,9 @@ const validateRange = (value: number | undefined, min: number, max: number): Non }; const getLensModel = (exifTags: ImmichTags): string | null => { - const lensModel = (exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '').trim(); + const lensModel = String( + exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '', + ).trim(); if (lensModel === '----') { return null; } diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts index 029462a865..b947f3795a 100644 --- a/server/src/services/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -54,6 +54,7 @@ describe(PartnerService.name, () => { const auth = AuthFactory.create({ id: user1.id }); mocks.partner.get.mockResolvedValue(void 0); + mocks.user.get.mockResolvedValue(user2); mocks.partner.create.mockResolvedValue(getForPartner(partner)); await expect(sut.create(auth, { sharedWithId: user2.id })).resolves.toBeDefined(); @@ -76,6 +77,19 @@ describe(PartnerService.name, () => { expect(mocks.partner.create).not.toHaveBeenCalled(); }); + + it('should throw an error when sharedWithId does not resolve to an existing (non-deleted) user', async () => { + const user1 = UserFactory.create(); + const user2 = UserFactory.create(); + const auth = AuthFactory.create({ id: user1.id }); + + mocks.partner.get.mockResolvedValue(void 0); + mocks.user.get.mockResolvedValue(void 0); + + await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException); + + expect(mocks.partner.create).not.toHaveBeenCalled(); + }); }); describe('remove', () => { diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index cc950edb5b..26d7701077 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -16,6 +16,12 @@ export class PartnerService extends BaseService { throw new BadRequestException(`Partner already exists`); } + const user = await this.userRepository.get(sharedWithId, {}); + if (!user) { + this.logger.debug('Partner creation failed: user not found'); + throw new BadRequestException('Invalid user'); + } + const partner = await this.partnerRepository.create(partnerId); return this.mapPartner(partner, PartnerDirection.SharedBy); } diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index e1575a496a..b4b1af35d6 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -1,5 +1,6 @@ import { SystemMetadataKey } from 'src/enum'; import { ServerService } from 'src/services/server.service'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTestService, ServiceMocks } from 'test/utils'; describe(ServerService.name, () => { @@ -161,7 +162,7 @@ describe(ServerService.name, () => { oauthButtonText: 'Login with OAuth', trashDays: 30, userDeleteDelay: 7, - isInitialized: undefined, + isInitialized: false, isOnboarded: false, externalDomain: '', publicUsers: true, @@ -172,6 +173,19 @@ describe(ServerService.name, () => { }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); + + it('should be initialized once an admin exists', async () => { + mocks.user.hasAdmin.mockResolvedValue(true); + + await expect(sut.getSystemConfig()).resolves.toMatchObject({ isInitialized: true }); + }); + + it('should be initialized when setup is disabled', async () => { + mocks.config.getEnv.mockReturnValue(mockEnvData({ setup: { allow: false } })); + mocks.user.hasAdmin.mockResolvedValue(false); + + await expect(sut.getSystemConfig()).resolves.toMatchObject({ isInitialized: true }); + }); }); describe('getStats', () => { diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index ad212292ba..57342f9509 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -111,9 +111,8 @@ export class ServerService extends BaseService { } async getSystemConfig(): Promise { - const { setup } = this.configRepository.getEnv(); const config = await this.getConfig({ withCache: false }); - const isInitialized = !setup.allow || (await this.userRepository.hasAdmin()); + const isInitialized = !(await this.isSetupAvailable()); const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.AdminOnboarding); return { diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 87fc40306f..e3842b1503 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -441,7 +441,12 @@ export class SyncService extends BaseService { const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { const albumUsers = await this.syncRepository.album.getAlbumUsers(data.id); - send(response, { type: upsertType, ids: [updateId], data: syncAlbumV2ToV1(data, albumUsers) }); + send(response, { + type: upsertType, + ids: [updateId], + // TODO: return null instead of '' in v4 + data: syncAlbumV2ToV1({ ...data, description: data.description ?? '' }, albumUsers), + }); } } @@ -455,7 +460,8 @@ export class SyncService extends BaseService { const upsertType = SyncEntityType.AlbumV2; const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { - send(response, { type: upsertType, ids: [updateId], data }); + // TODO: return null instead of '' in v4 + send(response, { type: upsertType, ids: [updateId], data: { ...data, description: data.description ?? '' } }); } } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index d5404048c0..08851da96a 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -362,7 +362,7 @@ describe(SystemConfigService.name, () => { expect(mocks.logger.error).toHaveBeenCalledTimes(2); expect(mocks.logger.error.mock.calls[0][0]).toEqual('Unable to load configuration file: immich-config.json'); expect(mocks.logger.error.mock.calls[1][0].toString()).toEqual( - expect.stringContaining('YAMLException: duplicated mapping key (1:20)'), + expect.stringContaining('YAMLException: duplicated mapping key (1:21)'), ); }); diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts index b416b3b904..9b75bc85ec 100644 --- a/server/test/medium/responses.ts +++ b/server/test/medium/responses.ts @@ -35,7 +35,4 @@ export const errorDto = { incorrectLogin: { message: 'Incorrect email or password', }, - alreadyHasAdmin: { - message: 'The server already has an admin', - }, }; diff --git a/server/test/medium/specs/services/auth.service.spec.ts b/server/test/medium/specs/services/auth.service.spec.ts index 1fc306f790..5e5c880955 100644 --- a/server/test/medium/specs/services/auth.service.spec.ts +++ b/server/test/medium/specs/services/auth.service.spec.ts @@ -57,16 +57,6 @@ describe(AuthService.name, () => { }), ); }); - - it('should not allow a second admin to sign up', async () => { - const { sut, ctx } = setup(); - await ctx.newUser({ isAdmin: true }); - const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' }; - - const response = sut.adminSignUp(dto); - await expect(response).rejects.toThrow(BadRequestException); - await expect(response).rejects.toThrow('The server already has an admin'); - }); }); describe('login', () => { diff --git a/server/test/medium/specs/services/metadata.service.spec.ts b/server/test/medium/specs/services/metadata.service.spec.ts index 6dc66e3ed5..37603520f7 100644 --- a/server/test/medium/specs/services/metadata.service.spec.ts +++ b/server/test/medium/specs/services/metadata.service.spec.ts @@ -152,4 +152,23 @@ describe(MetadataService.name, () => { ).resolves.toEqual({ dateTimeOriginal: new Date('4260-03-05T04:04:12.000Z') }); }); }); + + it('should handle float lens models (#30492)', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + const { filePath } = await createTestFile({ LensModel: 1.8 }); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ originalPath: filePath, ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: '' }); + + await sut.handleMetadataExtraction({ id: asset.id }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .where('assetId', '=', asset.id) + .select('lensModel') + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lensModel: '1.8' }); + }); }); diff --git a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts index a0802abe73..0fd58c527c 100644 --- a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts @@ -161,12 +161,14 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { // backfill needs assets with an older updateId const { asset: sharedAsset1 } = await ctx.newAsset({ ownerId: user2.id }); + await wait(2); const { asset: sharedAsset2 } = await ctx.newAsset({ ownerId: user2.id }); await wait(2); const { album: sharedAlbum } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: sharedAlbum.id, assetId: sharedAsset1.id }); + await wait(2); await ctx.newAlbumAsset({ albumId: sharedAlbum.id, assetId: sharedAsset2.id }); await wait(2); diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 1ad40ecca7..8bd65af73f 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -406,6 +406,101 @@ describe('core plugin', () => { }); }); + describe('assetFileFilter', () => { + it('should match assets case-insensitively', async () => { + const { user } = await ctx.newUser(); + const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([ + ctx.newAsset({ ownerId: user.id, originalFileName: 'exampleFile.png' }), + ctx.newAsset({ ownerId: user.id, originalFileName: 'anotherfile.jpg' }), + ]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [ + { + method: 'immich-plugin-core#assetFileFilter', + config: { matchType: 'contains', pattern: 'File' }, + }, + { + method: 'immich-plugin-core#assetFavorite', + }, + ], + }); + + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset1.id }), + ).resolves.toBeUndefined(); + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id }), + ).resolves.toBeUndefined(); + await expect(ctx.get(AssetRepository).getById(asset1.id)).resolves.toMatchObject({ isFavorite: true }); + await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: true }); + }); + + it('should match assets by regex', async () => { + const { user } = await ctx.newUser(); + const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([ + ctx.newAsset({ ownerId: user.id, originalFileName: 'exampleFile.png' }), + ctx.newAsset({ ownerId: user.id, originalFileName: 'anotherfile.jpg' }), + ]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [ + { + method: 'immich-plugin-core#assetFileFilter', + config: { matchType: 'regex', pattern: '.+png' }, + }, + { + method: 'immich-plugin-core#assetFavorite', + }, + ], + }); + + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset1.id }), + ).resolves.toBeUndefined(); + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id }), + ).resolves.toBeUndefined(); + await expect(ctx.get(AssetRepository).getById(asset1.id)).resolves.toMatchObject({ isFavorite: true }); + await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: false }); + }); + + it('should filter assets by path if specified', async () => { + const { user } = await ctx.newUser(); + const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([ + ctx.newAsset({ ownerId: user.id, originalPath: '/library/folder/file1.png' }), + ctx.newAsset({ ownerId: user.id, originalPath: '/library/file2.png' }), + ]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [ + { + method: 'immich-plugin-core#assetFileFilter', + config: { matchType: 'contains', pattern: 'folder', usePath: true }, + }, + { + method: 'immich-plugin-core#assetFavorite', + }, + ], + }); + + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset1.id }), + ).resolves.toBeUndefined(); + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id }), + ).resolves.toBeUndefined(); + await expect(ctx.get(AssetRepository).getById(asset1.id)).resolves.toMatchObject({ isFavorite: true }); + await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: false }); + }); + }); + describe('assetTypeFilter', () => { it('should favorite asset if it is a video', async () => { const { user } = await ctx.newUser(); diff --git a/server/test/utils.ts b/server/test/utils.ts index b633cbc4de..2f32f3b412 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -89,6 +89,7 @@ import { assert, Mock, Mocked, vitest } from 'vitest'; export type ControllerContext = { authenticate: Mock; + requireSetupAvailable: Mock; getHttpServer: () => any; reset: () => void; close: () => Promise; @@ -124,7 +125,7 @@ export const controllerSetup = async (controller: new (...args: any[]) => unknow { provide: APP_GUARD, useClass: AuthGuard }, { provide: LoggingRepository, useValue: LoggingRepository.create() }, { provide: ClsService, useValue: { getId: vi.fn() } }, - { provide: AuthService, useValue: { authenticate: vi.fn() } }, + { provide: AuthService, useValue: { authenticate: vi.fn(), requireSetupAvailable: vi.fn() } }, ...providers, ], }) @@ -137,13 +138,17 @@ export const controllerSetup = async (controller: new (...args: any[]) => unknow await app.init(); // allow the AuthController to override the AuthService itself - const authenticate = app.get>(AuthService).authenticate as Mock; + const resolvedAuthService = app.get>(AuthService); + const authenticate = resolvedAuthService.authenticate as Mock; + const requireSetupAvailable = resolvedAuthService.requireSetupAvailable as Mock; return { authenticate, + requireSetupAvailable, getHttpServer: () => app.getHttpServer(), reset: () => { authenticate.mockReset(); + requireSetupAvailable.mockReset(); }, close: async () => { await app.close(); @@ -184,10 +189,12 @@ export const automock = ( const mocks: Mock[] = []; const instance = new Dependency(...args); - const propertyNames = new Set([ - ...Object.getOwnPropertyNames(Dependency.prototype), - ...Object.getOwnPropertyNames(instance), - ]); + const propertyNames = new Set(Object.getOwnPropertyNames(instance)); + for (let proto = Dependency.prototype; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) { + for (const property of Object.getOwnPropertyNames(proto)) { + propertyNames.add(property); + } + } for (const property of propertyNames) { if (property === 'constructor') { continue; diff --git a/web/mise.toml b/web/mise.toml index b0d41317cb..7b9e3c2f3b 100644 --- a/web/mise.toml +++ b/web/mise.toml @@ -11,8 +11,12 @@ run = "pnpm run build:stats" run = "pnpm run preview" [tasks.start] -depends = [":install", "//:sdk:install", "//:sdk:build"] -run = "pnpm run dev" +run = [ + { task = ":install" }, + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, + "pnpm run dev", +] [tasks."start-demo"] env.IMMICH_SERVER_URL = "https://demo.immich.app" @@ -43,8 +47,9 @@ run = "pnpm run check:svelte" run = { tasks = [":check-typescript", ":check-svelte"] } [tasks.ci-unit] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":format" }, { task = ":check" }, diff --git a/web/package.json b/web/package.json index b23bc94edb..338e2f5abe 100644 --- a/web/package.json +++ b/web/package.json @@ -80,7 +80,7 @@ "@sveltejs/kit": "^2.56.1", "@sveltejs/vite-plugin-svelte": "7.2.0", "@tailwindcss/vite": "^4.2.4", - "@testing-library/jest-dom": "^6.4.2", + "@testing-library/jest-dom": "^7.0.0", "@testing-library/svelte": "^5.2.8", "@testing-library/user-event": "^14.5.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2", @@ -105,7 +105,7 @@ "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^4.0.0", "rollup-plugin-visualizer": "^7.0.0", - "svelte": "5.56.5", + "svelte": "5.56.8", "svelte-check": "^4.4.6", "svelte-eslint-parser": "^1.3.3", "tailwindcss": "^4.2.4", diff --git a/web/src/lib/components/SchemaAlbumPicker.svelte b/web/src/lib/components/SchemaAlbumPicker.svelte index b7eb9dd7dd..e4eb271a3d 100644 --- a/web/src/lib/components/SchemaAlbumPicker.svelte +++ b/web/src/lib/components/SchemaAlbumPicker.svelte @@ -14,7 +14,7 @@ let { array, label, description, albumIds = $bindable([]) }: Props = $props(); const onAlbums = async () => { - const albums = await modalManager.show(AlbumPickerModal); + const albums = await modalManager.show(AlbumPickerModal, {}); if (!albums || albums.length === 0) { return; } diff --git a/web/src/lib/components/asset-viewer/AlbumListItem.svelte b/web/src/lib/components/asset-viewer/AlbumListItem.svelte index af40deb27a..3d4be59a2c 100644 --- a/web/src/lib/components/asset-viewer/AlbumListItem.svelte +++ b/web/src/lib/components/asset-viewer/AlbumListItem.svelte @@ -40,6 +40,9 @@ const albumNameArray: string[] = $derived.by(() => { let { albumName } = album; let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery)); + if (findIndex === -1) { + return [albumName, '', '']; + } let findLength = searchQuery.length; return [ albumName.slice(0, findIndex), diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts b/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts index 669ce22f85..c7c3413b84 100644 --- a/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts +++ b/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts @@ -108,6 +108,19 @@ describe('Album Modal', () => { ]); }); + it('search matches on description as well as name', () => { + const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc); + const holidayAlbum = albumFactory.build({ albumName: 'Vacances 2019', description: 'Crete' }); + const constructionAlbum = albumFactory.build({ albumName: 'Construction' }); + const modalRows = converter.toModalRows('Crete', [], [holidayAlbum, constructionAlbum], -1, []); + + expect(modalRows).toStrictEqual([ + createNewAlbumRow(false), + createSectionRow('ALBUMS'), + createAlbumRow(holidayAlbum, false), + ]); + }); + it('selection can select new album row', () => { const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc); const holidayAlbum = albumFactory.build({ albumName: 'Holidays' }); diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts b/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts index ad96b27c06..5d4e46a063 100644 --- a/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts +++ b/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts @@ -46,10 +46,14 @@ export class AlbumModalRowConverter { const recentAlbumsToShow = search.length === 0 ? recentAlbums : []; const rows: AlbumModalRow[] = [{ type: AlbumModalRowType.NEW_ALBUM, selected: selectedRowIndex === 0 }]; + const normalizedSearch = normalizeSearchString(search); const filteredAlbums = sortAlbums( search.length > 0 && albums.length > 0 ? albums.filter((album) => { - return normalizeSearchString(album.albumName).includes(normalizeSearchString(search)); + return ( + normalizeSearchString(album.albumName).includes(normalizedSearch) || + normalizeSearchString(album.description).includes(normalizedSearch) + ); }) : albums, { sortBy: this.sortBy, orderBy: this.orderBy }, diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte index 7cbfd67ddf..423f0bec55 100644 --- a/web/src/lib/components/timeline/Month.svelte +++ b/web/src/lib/components/timeline/Month.svelte @@ -61,10 +61,8 @@ {@const isTimelineDaySelected = assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
{ - const success = await handleUpdateAlbum(album, { albumName, description }); + const success = await handleUpdateAlbum(album, { albumName, description: description || null }); if (success) { onClose(); } diff --git a/web/src/lib/modals/AlbumPickerModal.spec.ts b/web/src/lib/modals/AlbumPickerModal.spec.ts new file mode 100644 index 0000000000..4245f3e17f --- /dev/null +++ b/web/src/lib/modals/AlbumPickerModal.spec.ts @@ -0,0 +1,58 @@ +import { render, screen, waitFor } from '@testing-library/svelte'; +import { init, register, waitLocale } from 'svelte-i18n'; +import { getAnimateMock } from '$lib/__mocks__/animate.mock'; +import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock'; +import { sdkMock } from '$lib/__mocks__/sdk.mock'; +import { getVisualViewportMock } from '$lib/__mocks__/visual-viewport.mock'; +import AlbumPickerModal from './AlbumPickerModal.svelte'; + +describe('AlbumPickerModal component', () => { + const onClose = vi.fn(); + + beforeAll(async () => { + await init({ fallbackLocale: 'en-US' }); + register('en-US', () => import('$i18n/en.json')); + await waitLocale('en-US'); + }); + + beforeEach(() => { + vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock()); + vi.stubGlobal('visualViewport', getVisualViewportMock()); + vi.resetAllMocks(); + Element.prototype.animate = getAnimateMock(); + }); + + afterAll(async () => { + await waitFor(() => { + expect(document.body.style.pointerEvents).not.toBe('none'); + }); + }); + + it('shows the singular selection count title when selectedItemsCount is 1', async () => { + // Called by onMount() + sdkMock.getAllAlbums.mockResolvedValueOnce([]); + + render(AlbumPickerModal, { props: { onClose, selectedItemsCount: 1 } }); + + expect(await screen.findByText('Add 1 item to album')).toBeInTheDocument(); + expect(screen.queryByText('Select albums')).not.toBeInTheDocument(); + }); + + it('shows the plural selection count title when selectedItemsCount is greater than 1', async () => { + sdkMock.getAllAlbums.mockResolvedValueOnce([]); + + render(AlbumPickerModal, { props: { onClose, selectedItemsCount: 3 } }); + + expect(await screen.findByText('Add 3 items to album')).toBeInTheDocument(); + expect(screen.queryByText('Select albums')).not.toBeInTheDocument(); + }); + + it('shows the generic title when selectedItemsCount is not provided', async () => { + sdkMock.getAllAlbums.mockResolvedValueOnce([]); + + render(AlbumPickerModal, { props: { onClose } }); + + expect(await screen.findByText('Select albums')).toBeInTheDocument(); + expect(screen.queryByText('Add 1 item to album')).not.toBeInTheDocument(); + }); +}); diff --git a/web/src/lib/modals/AlbumPickerModal.svelte b/web/src/lib/modals/AlbumPickerModal.svelte index 561deeac23..19430e271c 100644 --- a/web/src/lib/modals/AlbumPickerModal.svelte +++ b/web/src/lib/modals/AlbumPickerModal.svelte @@ -23,9 +23,10 @@ type Props = { onClose: (albums?: AlbumResponseDto[]) => void; + selectedItemsCount?: number; }; - let { onClose }: Props = $props(); + let { onClose, selectedItemsCount }: Props = $props(); onMount(async () => { albums = await getAllAlbums({}); @@ -147,9 +148,15 @@ } } }; + + const title = $derived( + selectedItemsCount === undefined + ? $t('select_albums') + : $t('add_to_album_item_count', { values: { count: selectedItemsCount } }), + ); - +
{#if loading} diff --git a/web/src/lib/modals/AppDownloadModal.svelte b/web/src/lib/modals/AppDownloadModal.svelte index 84d7630909..01a998bf5c 100644 --- a/web/src/lib/modals/AppDownloadModal.svelte +++ b/web/src/lib/modals/AppDownloadModal.svelte @@ -1,5 +1,5 @@ - + diff --git a/web/src/lib/modals/PersonMergeSuggestionModal.svelte b/web/src/lib/modals/PersonMergeSuggestionModal.svelte index c449ed35cf..94006ed5e7 100644 --- a/web/src/lib/modals/PersonMergeSuggestionModal.svelte +++ b/web/src/lib/modals/PersonMergeSuggestionModal.svelte @@ -63,13 +63,16 @@
{#if !choosePersonToMerge}
- + + {#key personToMerge.id} + + {/key}
@@ -101,14 +104,16 @@ } }} > - 0} - circle - shadow - url={getPeopleThumbnailUrl(personToBeMergedInto)} - altText={personToBeMergedInto.name} - widthStyle="100%" - /> + {#key personToBeMergedInto.id} + 0} + circle + shadow + url={getPeopleThumbnailUrl(personToBeMergedInto)} + altText={personToBeMergedInto.name} + widthStyle="100%" + /> + {/key} {:else}
@@ -116,6 +121,7 @@
+
{#each potentialMergePeople as person (person.id)}
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte index 1d80fcb766..cb2fb2a9bb 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte @@ -20,7 +20,7 @@ const response = await updateAlbumInfo({ id, updateAlbumDto: { - description, + description: description || null, }, }); eventManager.emit('AlbumUpdate', response); diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte index 4a594ddce1..8f2f82e9b5 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte @@ -40,7 +40,10 @@ class:dark:border-immich-dark-primary={border} class:border-immich-primary={border} > - + + {#key person.id} + + {/key}