diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index c2664839e38cd..d924c84596a8e 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 05c6f82977e7b..515089619245f 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -74,7 +74,7 @@ jobs: # 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/test.yml b/.github/workflows/test.yml index b9fb652f59fb9..a01b429713649 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/deployment/mise.lock b/deployment/mise.lock index 663ae4985da40..018620a4a511e 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 2e26da09f6db4..7098b79c99e68 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 aecb6b1dad053..013ff266fa71a 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 c8ebeffbcd001..5c34acdd9de50 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 dbfd2fb11249b..c10a858ed9c6f 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 384d3d11d7b43..3ce98dc2e10b2 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/e2e/mise.toml b/e2e/mise.toml index b14992256446d..487f4ebf46641 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 5fd887c44bda5..1b3447767e9f5 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 cf6d752561e47..e757c721d64de 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/mise.lock b/mise.lock index 656d11b3eb120..3b05f22889140 100644 --- a/mise.lock +++ b/mise.lock @@ -252,43 +252,43 @@ 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" diff --git a/mise.toml b/mise.toml index 5ff9db581997d..d069dc1b995ce 100644 --- a/mise.toml +++ b/mise.toml @@ -18,7 +18,7 @@ config_roots = [ node = "24.15.0" pnpm = "11.13.1" 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" diff --git a/mobile/.gitignore b/mobile/.gitignore index 64aa4a8dd7d18..bdddf7bbccc46 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -34,6 +34,11 @@ lib/**/*.drift.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 1a7b463913fa8..e828760789e24 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -28,8 +28,6 @@ linter: rules: # Formatting avoid_print: true - unawaited_futures: true - use_build_context_synchronously: false require_trailing_commas: true unrelated_type_equality_checks: true prefer_const_constructors: true @@ -51,6 +49,29 @@ linter: 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 analyzer: diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt deleted file mode 100644 index 3fcaed34bcd53..0000000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt +++ /dev/null @@ -1,453 +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 BackgroundWorkerPigeonUtils { - - fun createConnectionError(channelName: String): FlutterError { - return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } - - 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 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 4e2e382c2b32a..0000000000000 --- 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 aec1f06164324..0000000000000 --- 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 c380a0a6a5535..0000000000000 --- 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 e741ce07e9780..0000000000000 --- 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 2b5f4d2f57ee4..0000000000000 --- 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 5f7bf806b4a39..0000000000000 --- 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 02f1cb237d28c..0000000000000 --- 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/viewintent/ViewIntent.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt deleted file mode 100644 index 1d5af15cb4f86..0000000000000 --- 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 a4cf562bcbdde..1ce643be124a3 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 bd01e953f9b52..0000000000000 --- 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 c7aff63e10ede..0000000000000 --- 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 265923d165d39..0000000000000 --- 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 b9324260be014..0000000000000 --- 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/RemoteImages.g.swift b/mobile/ios/Runner/Images/RemoteImages.g.swift deleted file mode 100644 index 12eaaeec60c59..0000000000000 --- 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 b9c116f0c5593..0000000000000 --- 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 a752785c5b79f..0000000000000 --- 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/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index b73f79825552e..0e702ba868a7f 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -23,7 +23,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 +195,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, ); } diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index f4c4a519d3461..f35c962ff05ed 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -171,13 +171,4 @@ class AssetService { 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 e4c332b283232..b0dbf8fbeac62 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; diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index feb104f90dcbb..b4ebc66f89ee5 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/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 9b539ec218153..b20ba306ff730 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); - - 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; + unawaited( + _mutex.run(() async { + final totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); + + 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()); + }), + ); }); } diff --git a/mobile/lib/domain/utils/event_stream.dart b/mobile/lib/domain/utils/event_stream.dart index 5967fdca50c2d..0069c75f868bc 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/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 87a05ab8fe2a5..58e1a91d332cb 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/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index ede3de3966b03..da2070fd712d4 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/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index cdf8bfa15bae5..e97b05465bec8 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -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/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 3a63812485a13..9500190abc8ca 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_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 844226d49f63b..c43de69c5d3c1 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -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/main.dart b/mobile/lib/main.dart index 09bcdc752f264..317733f1deecd 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -128,17 +128,17 @@ 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()); case AppLifecycleState.inactive: dPrint(() => "[APP STATE] inactive"); ref.read(appStateProvider.notifier).handleAppInactivity(); case AppLifecycleState.paused: dPrint(() => "[APP STATE] paused"); - ref.read(appStateProvider.notifier).handleAppPause(); + unawaited(ref.read(appStateProvider.notifier).handleAppPause()); case AppLifecycleState.detached: dPrint(() => "[APP STATE] detached"); - ref.read(appStateProvider.notifier).handleAppDetached(); + unawaited(ref.read(appStateProvider.notifier).handleAppDetached()); case AppLifecycleState.hidden: dPrint(() => "[APP STATE] hidden"); ref.read(appStateProvider.notifier).handleAppHidden(); @@ -147,27 +147,32 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve Future initApp() async { WidgetsBinding.instance.addObserver(this); - // Draw the app from edge to edge unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); + await _setNavigationBarColor(); + + await FlutterLocalNotificationsPlugin().initialize( + const InitializationSettings( + android: AndroidInitializationSettings('@drawable/notification_icon'), + iOS: DarwinInitializationSettings(), + ), + ); + } - // Sets the navigation bar color + 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); - - await FlutterLocalNotificationsPlugin().initialize( - const InitializationSettings( - android: AndroidInitializationSettings('@drawable/notification_icon'), - iOS: DarwinInitializationSettings(), - ), - ); } Future _deepLinkBuilder(PlatformDeepLink deepLink) async { @@ -216,17 +221,19 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve @override 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, + ), + ); } }); @@ -243,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/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 793437579af87..cc618dbe60696 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) { @@ -69,7 +69,7 @@ class _DriftBackupPageState extends ConsumerState { @override void dispose() { super.dispose(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); } @override @@ -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), @@ -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( 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 9f60a4e19322c..7667dbc3f115f 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -132,6 +132,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 +393,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 +414,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 +433,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 +463,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 +483,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 b35abd198eb57..4f66d6d6fb286 100644 --- a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart +++ b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart @@ -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 42643a0496da6..ef4974d4ddd06 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -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; } diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index 3c2570a434416..5ed43672d958e 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)), diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 5458d90808870..b04d8dc926f3c 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.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'; @@ -61,7 +63,7 @@ class AppLogPage extends HookConsumerWidget { size: 20.0, ), onPressed: () { - immichLogger.clearLogs(); + unawaited(immichLogger.clearLogs()); shouldReload.value = !shouldReload.value; }, ), @@ -70,7 +72,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 +80,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), ), diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 274231a72995e..52f792df9d658 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.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'; @@ -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)), @@ -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)), diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart index f39aa07166c49..267d32fc3dac9 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'; @@ -15,7 +17,7 @@ class DownloadPanel extends ConsumerWidget { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); void onCancelDownload(String id) { - ref.watch(downloadStateProvider.notifier).cancelDownload(id); + unawaited(ref.watch(downloadStateProvider.notifier).cancelDownload(id)); } return Positioned( diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart index 4563834473a93..58528008cae83 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 711783bc94fab..2c39883b7e96f 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -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) { @@ -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()); - - bool syncSuccess = false; - await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), - ]); - - 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 (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()]), - }, - ), + 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), + ]); + + await viewIntentHandler.flushDeferredViewIntent(); + + 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'); + unawaited(ref.read(authProvider.notifier).logout()); + if (!mounted) { + return; + } + + 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 2fdcec4054954..f834e4ac51563 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 6934d7b6c52d4..69631efe4eb40 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; } @@ -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]); @@ -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 7beda1d47b28f..81e08d853b1f1 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())); } - void 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 7274b8a14ef14..afe64d24f4e32 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 a4f52ebd4ce1f..18fc235dd8b6f 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/login.page.dart b/mobile/lib/pages/login/login.page.dart index 79091d26792fa..e225c4c066dd1 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.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'; @@ -22,7 +24,7 @@ class LoginPage extends HookConsumerWidget { } useEffect(() { - getAppInfo(); + unawaited(getAppInfo()); return null; }); @@ -55,7 +57,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 96f41a4d3872d..bb848b24bc62c 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; @@ -37,7 +38,7 @@ class MapLocationPickerPage extends HookConsumerWidget { } void onClose([LatLng? selected]) { - context.maybePop(selected); + unawaited(context.maybePop(selected)); } Future getCurrentLocation() async { diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index ec88c4a9e4b16..db5e583d7acc0 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'; @@ -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), ), 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 34f4c41b48c5f..0000000000000 --- 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 c7836c4c69d61..0000000000000 --- 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 8cf8979532c94..0000000000000 --- 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 fbd0876735042..0000000000000 --- 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 bd979af87b080..0000000000000 --- 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 6258060bfb115..0000000000000 --- 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 7b85d611d292c..0000000000000 --- 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 5239cb3e4535f..0000000000000 --- 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 53d7b10fc336b..0000000000000 --- 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 d457c249de9bc..0000000000000 --- 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 5ceb2f855d5d8..072c2524be89f 100644 --- a/mobile/lib/presentation/actions/action.dart +++ b/mobile/lib/presentation/actions/action.dart @@ -1,32 +1,54 @@ +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'; - -class ActionScope { - final BuildContext context; - final WidgetRef ref; - final UserDto authUser; - - const ActionScope({required this.context, required this.ref, required this.authUser}); +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 ActionItem { + final IconData icon; + final String label; + final FutureOr Function() onAction; + final FutureOr Function()? onSecondaryAction; + + 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; + // null when the action is not applicable for the current context + ActionItem? create(BuildContext context, WidgetRef ref); +} - String label(ActionScope scope); +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 {}, + }, + }), +); - bool isVisible(ActionScope scope) => true; +final clearSelectionProvider = Provider.family.autoDispose((ref, source) { + if (source == .timeline) { + return ref.read(multiSelectProvider.notifier).reset; + } - Future onAction(ActionScope scope); -} + return () {}; +}); -abstract class AssetAction extends BaseAction { - final Iterable assets; +final ownedAssetsActionProvider = Provider.family.autoDispose, ActionSource>( + (ref, source) => ref.watch(assetsActionProvider(source)).owned(ref.watch(authUserProvider).id), +); - const AssetAction({required this.assets}); +abstract class AssetActionBuilder extends ActionBuilder { + final ActionSource source; - Iterable filter(ActionScope scope) => assets.whereType(); + const AssetActionBuilder({required this.source}); } diff --git a/mobile/lib/presentation/actions/action.widget.dart b/mobile/lib/presentation/actions/action.widget.dart index eba5e3939c72d..a865528440d8f 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}); -} - -class _ActionWidget extends ConsumerWidget { - final BaseAction action; - final Widget Function(_ActionWidgetScope context) builder; + const ActionWidget({super.key, required this.action}); - 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; - final ImmichVariant variant; - - const ActionIconButtonWidget({super.key, required this.action, this.variant = .ghost}); +class ActionColumnButton extends ActionWidget { + const ActionColumnButton({super.key, required super.action}); @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) => ImmichColumnButton( + icon: action.icon, + label: action.label, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, ); } -class ActionButtonWidget extends StatelessWidget { - final BaseAction action; +class ActionIconButton extends ActionWidget { final ImmichVariant variant; - const ActionButtonWidget({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) => - ImmichTextButton(labelText: ctx.label, 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 ActionColumnButtonWidget extends StatelessWidget { - final BaseAction action; +class ActionButton extends ActionWidget { + final ImmichVariant variant; - const ActionColumnButtonWidget({super.key, required this.action}); + const ActionButton({super.key, required super.action, this.variant = .ghost}); @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => ImmichColumnButton(icon: action.icon, label: ctx.label, onPressed: ctx.onAction), + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => ImmichTextButton( + labelText: action.label, + icon: action.icon, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, + variant: variant, ); } -class ActionMenuItemWidget extends StatelessWidget { - final BaseAction action; - - const ActionMenuItemWidget({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) => 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/asset_debug.action.dart b/mobile/lib/presentation/actions/asset_debug.action.dart index aec99fc90b142..ff2935fc967c9 100644 --- a/mobile/lib/presentation/actions/asset_debug.action.dart +++ b/mobile/lib/presentation/actions/asset_debug.action.dart @@ -2,26 +2,27 @@ import 'dart:async'; import 'package:auto_route/auto_route.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 assets = ref.watch(assetsActionProvider(source)).assets; + final troubleshootEnabled = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); + if (!troubleshootEnabled || assets.length != 1) { + 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: assets.single))), + ); + } } diff --git a/mobile/lib/presentation/actions/favorite.action.dart b/mobile/lib/presentation/actions/favorite.action.dart index a480e10a5f463..9ab37701630bf 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; + } - @override - IconData get icon => favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded; + 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); +}); - @override - String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite; +class FavoriteAction extends AssetActionBuilder { + const FavoriteAction({required super.source}); @override - Iterable filter(ActionScope scope) => - AssetFilter(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite); - - @override - bool isVisible(ActionScope scope) => filter(scope).isNotEmpty; + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldFavorite = ref.watch(_stateProvider(source).select((state) => state?.shouldFavorite)); + if (shouldFavorite == null) { + return null; + } + + return .new( + icon: shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, + label: shouldFavorite ? context.t.favorite : context.t.unfavorite, + onAction: () => _favorite(context, ref), + ); + } - @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); + Future _favorite(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final _State(:shouldFavorite, :assetIds) = state; + final message = shouldFavorite + ? context.t.favorite_action_prompt(count: assetIds.length) + : context.t.unfavorite_action_prompt(count: assetIds.length); + final assertService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await assertService.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/partner.action.dart b/mobile/lib/presentation/actions/partner.action.dart index 11fb69ee757f5..012340c1d8178 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; - - @override - String label(ActionScope scope) => scope.context.t.remove; + ActionItem create(BuildContext context, WidgetRef ref) => + ActionItem(icon: Icons.person_remove_rounded, label: context.t.remove, onAction: () => _remove(context, ref)); - @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/timeline.action.dart b/mobile/lib/presentation/actions/timeline.action.dart deleted file mode 100644 index d8d367f6746e6..0000000000000 --- 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/pages/download_info.page.dart b/mobile/lib/presentation/pages/download_info.page.dart index af44714b8315d..c2c63c7860cce 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'; @@ -15,7 +17,7 @@ class DownloadInfoPage extends ConsumerWidget { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); void onCancelDownload(String id) { - ref.watch(downloadStateProvider.notifier).cancelDownload(id); + unawaited(ref.watch(downloadStateProvider.notifier).cancelDownload(id)); } return Scaffold( diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index a52f1d7358566..ebf7c2efa78e1 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; @@ -25,13 +27,17 @@ class DriftActivitiesPage extends HookConsumerWidget { 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( @@ -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 47a4625f8766f..ab91b6f3c868f 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 84060aa38c714..9c5161fa8a737 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -46,6 +46,10 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { 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(); @@ -72,17 +76,21 @@ 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, - ); + ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); + if (!context.mounted) { + return; } - ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); + ImmichToast.show( + context: context, + 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); } } @@ -110,18 +118,20 @@ 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]), + ), + ); + }, + ), ); } diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 3e5b603451eb7..cee0cdc334f66 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'; @@ -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 diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index 0dfae062dc6e5..93534d62a579e 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 190ad3af6a6f4..b2b4d250f19e8 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -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)), diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart index 9849558c941ef..e8130f7059b11 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 97062b88abbde..d36ccc350cc30 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 4ae97f30e1fdc..31371fe5815c9 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'; @@ -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()); }; }); void toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + 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((_) { @@ -88,7 +90,7 @@ class DriftMemoryPage extends HookConsumerWidget { // Go to the next asset 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(); @@ -100,7 +102,7 @@ class DriftMemoryPage extends HookConsumerWidget { // Go to the previous asset 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(); @@ -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 fd5b64c108236..70de434d9f4f0 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -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 f39b5e15c7a31..416b7d587ada5 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.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'; @@ -86,7 +88,7 @@ class _DriftPeopleCollectionPageState 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 5e4e525e06c4a..5d018561cc4d6 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()}), @@ -71,17 +77,21 @@ 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, - ); + ref.invalidate(remoteAlbumSharedUsersProvider(_album.id)); + if (!context.mounted) { + return; } - ref.invalidate(remoteAlbumSharedUsersProvider(_album.id)); + ImmichToast.show( + context: context, + 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); } } @@ -124,6 +134,9 @@ class _RemoteAlbumPageState extends ConsumerState { if (confirmed == true) { try { await ref.read(remoteAlbumProvider.notifier).deleteAlbum(_album.id); + if (!context.mounted) { + return; + } ImmichToast.show( context: context, @@ -133,6 +146,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), @@ -149,7 +166,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 ?? ''); }); @@ -247,20 +268,23 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { await ref .read(remoteAlbumProvider.notifier) .updateAlbum(widget.album.id, name: newTitle, description: newDescription); - - if (mounted) { - Navigator.of( - context, - ).pop(_EditAlbumData(name: newTitle, description: newDescription.isEmpty ? null : newDescription)); + if (!mounted) { + return; } + + 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, + ); } } diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 3f0c441c01560..81c09bea6732d 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -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()); } @@ -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,7 +147,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si }; if (!widget.timeline.hasRange(_nextIndex, 1)) { - widget.timeline.preloadAssets(_nextIndex); + unawaited(widget.timeline.preloadAssets(_nextIndex)); } } @@ -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(); @@ -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)), @@ -376,9 +378,9 @@ class _DriftSlideshowPageState extends ConsumerState with Si 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()); } } diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index 41394014a0c52..19a450b43519b 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'; @@ -53,7 +55,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { final sharedUsersList = useState>({}); void addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); + unawaited(context.maybePop(sharedUsersList.value.map((e) => e.id).toList())); } Widget buildTileIcon(UserDto user) { @@ -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(), ), ], diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart index 0ce9985c193d7..ec7d8ac3d792b 100644 --- a/mobile/lib/presentation/pages/edit/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -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(); } }, 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 3fb32b7d9316a..a987c9ca29dc1 100644 --- a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -74,7 +74,7 @@ class _ProfilePictureCropPageState extends ConsumerState .read(uploadProfileImageProvider.notifier) .upload(xFile, fileName: 'profile-picture.png'); - if (!context.mounted) { + if (!mounted) { return; } @@ -94,9 +94,7 @@ class _ProfilePictureCropPageState extends ConsumerState toastType: ToastType.success, ); - if (context.mounted) { - unawaited(context.maybePop()); - } + unawaited(context.maybePop()); } else { ImmichToast.show( context: context, @@ -106,7 +104,7 @@ class _ProfilePictureCropPageState extends ConsumerState ); } } catch (e) { - if (!context.mounted) { + if (!mounted) { return; } @@ -157,7 +155,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 6b818bd2737c2..39588cf051322 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -105,20 +105,26 @@ 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]); @@ -141,17 +147,19 @@ class DriftSearchPage extends HookConsumerWidget { 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), + ), ), ), ); @@ -176,17 +184,19 @@ class DriftSearchPage extends HookConsumerWidget { 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()), + ), ), ), ); @@ -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), + ), ), ), ), @@ -259,17 +271,19 @@ 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), + ), ), ), ); @@ -339,22 +353,24 @@ class DriftSearchPage extends HookConsumerWidget { } void 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); - }, + 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); + }, + ), ), ), ); @@ -383,13 +399,15 @@ 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), + ), ), ); } @@ -417,14 +435,16 @@ 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), + ), ), ); } @@ -462,13 +482,15 @@ 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), + ), ), ); } @@ -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()), ], ), ); @@ -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 fa2d5a06cfe95..e9839f31ea70b 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 86d3fa0749243..533bc72971860 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,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'; @@ -36,11 +38,11 @@ class _AddActionButtonState extends ConsumerState { case AddToMenuItem.album: _openAlbumSelector(); case AddToMenuItem.archive: - performArchiveAction(context, ref, source: ActionSource.viewer); + unawaited(performArchiveAction(context, ref, source: ActionSource.viewer)); case AddToMenuItem.unarchive: - performUnArchiveAction(context, ref, source: ActionSource.viewer); + unawaited(performUnArchiveAction(context, ref, source: ActionSource.viewer)); case AddToMenuItem.lockedFolder: - performMoveToLockFolderAction(context, ref, source: ActionSource.viewer); + unawaited(performMoveToLockFolderAction(context, ref, source: ActionSource.viewer)); } } @@ -112,21 +114,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, + ); + }, + ), ); } @@ -140,7 +144,7 @@ class _AddActionButtonState extends ConsumerState { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album); - if (!context.mounted) { + if (!mounted) { return; } @@ -171,7 +175,7 @@ class _AddActionButtonState extends ConsumerState { ); } - if (!context.mounted) { + if (!mounted) { return; } await Navigator.of(context).maybePop(); 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 index bb2cae21ad9ff..3322dd3a85933 100644 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart @@ -23,16 +23,17 @@ Future performArchiveAction(BuildContext context, WidgetRef ref, {required 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, - ); + if (!context.mounted) { + return; } + + final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); + 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 { 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 index 7a4f84fb4f2d3..9465f50500004 100644 --- a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.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'; @@ -21,7 +23,7 @@ class CastActionButton extends ConsumerWidget { iconColor: isCasting ? context.primaryColor : null, // null = default color label: "cast".t(context: context), onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); + unawaited(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 index 45dc5ec699119..a6ed4c42462d0 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart @@ -73,17 +73,17 @@ class DeleteActionButton extends ConsumerWidget { shouldRefreshStack ? ViewerStackAssetDeletedEvent(stackIndex: stackIndex) : const ViewerReloadAssetEvent(), ); } + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index 5a94d9807e52e..09969d8b8ad10 100644 --- 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 @@ -42,16 +42,17 @@ class DeleteLocalActionButton extends ConsumerWidget { 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, - ); + if (!context.mounted) { + return; } + + final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()}); + 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 diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart index 922f8593fadc8..c02fcf8f79f27 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart @@ -50,20 +50,20 @@ class DeletePermanentActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart index f3e048f06f7d0..3312e4f2a38f0 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart @@ -37,20 +37,20 @@ class DeleteTrashActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 efa7f5c6d0404..264e489db3c04 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 index b2b5050a8eba4..e93720186b817 100644 --- 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 @@ -24,20 +24,20 @@ class EditDateTimeActionButton extends ConsumerWidget { } ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index cc8e15617ce34..b250e325cea29 100644 --- 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 @@ -24,17 +24,17 @@ class EditLocationActionButton extends ConsumerWidget { } ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 0365335fd2424..0000000000000 --- 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}); - - Future _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/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart index 56191e9055b2f..6d5f5b387a902 100644 --- 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 @@ -22,20 +22,20 @@ Future performMoveToLockFolderAction(BuildContext context, WidgetRef ref, final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 { 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 index ebcfbaa1e5cc6..7049da13f8ad4 100644 --- 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 @@ -35,20 +35,20 @@ class RemoveFromAlbumActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index 75deef9ccb28f..ea0d9f384f3eb 100644 --- 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 @@ -27,20 +27,20 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).removeFromLockFolder(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index b752a77c89b1f..9270ce835150e 100644 --- a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart @@ -29,16 +29,17 @@ class RestoreActionButton extends ConsumerWidget { 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, - ); + if (!context.mounted) { + return; } + + final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); + 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 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 index 82a9d98549245..f6cd26c1896dd 100644 --- 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 @@ -19,17 +19,17 @@ class RestoreTrashActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).restoreTrash(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index d080efc5b2a4b..e6e572110eaf4 100644 --- a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart @@ -29,17 +29,17 @@ class SetAlbumCoverActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index c8dbb7cb1f9cf..5b41715022379 100644 --- 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 @@ -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'; @@ -13,12 +15,12 @@ class SetProfilePictureActionButton extends ConsumerWidget { const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context) { + Future _onTap(BuildContext context) async { if (!context.mounted) { return; } - context.pushRoute(ProfilePictureCropRoute(asset: asset)); + await context.pushRoute(ProfilePictureCropRoute(asset: asset)); } @override @@ -28,7 +30,7 @@ class SetProfilePictureActionButton extends ConsumerWidget { label: "set_as_profile_picture".t(context: context), iconOnly: iconOnly, menuItem: menuItem, - onPressed: () => _onTap(context), + onPressed: () => unawaited(_onTap(context)), maxWidth: 100, ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index ef520ea9413cb..eadcf0a81e52a 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -138,31 +138,33 @@ class ShareActionButton extends ConsumerWidget { 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(); - }); + unawaited( + 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; }, 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 index 479cf2dfe9b82..fdbc7a8cda0e1 100644 --- a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/slideshow_action_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'; @@ -12,12 +14,12 @@ class SlideshowActionButton extends ConsumerWidget { const SlideshowActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } - context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))); + await context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))); } @override @@ -27,7 +29,7 @@ class SlideshowActionButton extends ConsumerWidget { label: "slideshow".t(context: context), iconOnly: iconOnly, menuItem: menuItem, - onPressed: () => _onTap(context, ref), + onPressed: () => unawaited(_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 index b87d288a3e95d..026268fe52eb2 100644 --- a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart @@ -26,17 +26,17 @@ class StackActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).stack(user.id, source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index a320d3b1b1f19..2be2049a07a26 100644 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart @@ -31,17 +31,17 @@ class TrashActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).trash(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 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 index 78984f9ef1e75..552608f83fa25 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -25,16 +25,17 @@ Future performUnArchiveAction(BuildContext context, WidgetRef ref, {requir 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, - ); + if (!context.mounted) { + return; } + + final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); + 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 { 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 94d6588074383..0000000000000 --- 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}); - - Future _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 index c9a5102a9b5e2..47cdfe9b5f10d 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart @@ -22,17 +22,17 @@ class UnStackActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).unStack(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } 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, - ); - } + 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 diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 285c6290a9bf9..f30796e406d8c 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)), @@ -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), ); @@ -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; @@ -798,7 +802,7 @@ class CreateAlbumButton 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; } @@ -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; diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart index 1aeadf61bcc2f..bbf7e11e5a2b7 100644 --- a/mobile/lib/presentation/widgets/album/album_tile.dart +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -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 2701316e7502d..5622ed6b1b3cc 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/date_time_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart index 27bac68310295..2dc1c404566c2 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'; @@ -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 379f0975b140b..8edfca5bf1536 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,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; @@ -48,7 +50,7 @@ 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!)))); } } } 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 278da294c0e09..72db236b3cdad 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_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index 1ef22a891ffcd..7233356402f46 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(() {}); 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 3952dafdb2f93..23cc6119fb1c3 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; } @@ -223,7 +223,7 @@ class _AssetViewerState extends ConsumerState { case ViewerReloadAssetEvent(): _onViewerReloadEvent(); case final ViewerStackAssetDeletedEvent event: - _onViewerStackAssetDeletedEvent(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/ocr_overlay.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart index a9291f317356c..576d4937b6862 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/sheet_tile.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart index 69e84ee03d707..ac6f79a155412 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 d007883ec930f..63b5663d9d2bf 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; } @@ -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'); } @@ -292,7 +293,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_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 878a1c9405b70..deae9cbd07005 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'; @@ -44,7 +46,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { 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), @@ -52,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 3c9c0c692e424..85fa8b4563500 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 @@ -5,7 +5,6 @@ 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/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'; @@ -76,9 +75,6 @@ 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, @@ -89,7 +85,7 @@ class _ArchiveBottomSheetState extends ConsumerState { if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const UnArchiveActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) 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 d5ed3f6c9659f..a066e0167e026 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, + ), ); } }); 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 bcb9fc6fe32bf..6447c5ccf86b9 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 @@ -6,7 +6,6 @@ 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/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'; @@ -44,6 +43,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,9 +76,6 @@ 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, @@ -85,7 +84,7 @@ class FavoriteBottomSheet extends ConsumerWidget { const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ArchiveActionButton(source: ActionSource.timeline), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable 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 1949a79495b81..2f1ffe4cbc4c8 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,8 +4,7 @@ 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/asset_debug.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; +import 'package:immich_mobile/presentation/actions/favorite.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'; @@ -14,7 +13,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane 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'; @@ -83,9 +81,6 @@ 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, @@ -93,7 +88,7 @@ class _GeneralBottomSheetState extends ConsumerState { maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), @@ -101,7 +96,7 @@ class _GeneralBottomSheetState extends ConsumerState { isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline), - const FavoriteActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ArchiveActionButton(source: ActionSource.timeline), if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline), 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 a292c1899c749..f6cbc5eac92d7 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 @@ -5,7 +5,6 @@ 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/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'; @@ -85,9 +84,6 @@ 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, @@ -101,7 +97,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState if (ownsAlbum) ...[ const ArchiveActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), ], const DownloadActionButton(source: ActionSource.timeline), if (ownsAlbum) ...[ 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 2c89c68d990d6..d748452c0e1b7 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,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:easy_localization/easy_localization.dart'; @@ -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 78fc0a6a21db8..8c92ac2818cd2 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'; @@ -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 9cc386e302db1..927734ca2529b 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; @@ -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) { 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 b519da33c3102..966e070872009 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/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 847fa4e3819dd..90bb79cced7cf 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 7d71f0296d891..1d3dcb2cf0485 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -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 f1b4f80ec1ef8..eedf3aadf5491 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 a68a475ad44bd..918d64054bb75 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 3ce7b2e055f60..34116b144b213 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 { - final 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), diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index 2a88de8e0a059..7e782a3db4756 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -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 62889b10cb2f3..b0b78168890ed 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)) 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 6e66ff47ce36b..b5a9dd235c132 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 @@ -34,12 +34,16 @@ class _DriftPersonNameEditFormState extends ConsumerState(_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 2eaac2ebf5a34..bb63de369544d 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 @@ -32,12 +32,16 @@ class _DriftPersonNameEditFormState extends ConsumerState(newName); } } catch (error) { dPrint(() => 'Error updating name: $error'); - if (!context.mounted) { + if (!mounted) { return; } diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index 7712983fde901..16c947ca5e3fe 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -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( diff --git a/mobile/lib/presentation/widgets/timeline/header.widget.dart b/mobile/lib/presentation/widgets/timeline/header.widget.dart index d73d024efb3a4..76176041fa27c 100644 --- a/mobile/lib/presentation/widgets/timeline/header.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/header.widget.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'; @@ -105,7 +107,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 f5e3493a8e8a0..eb081a1e6a39b 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; }); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 5bd39deb8ab39..9e65dcb72c8b5 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -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/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 2b52973c0a769..8678f7c32e3f0 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -120,7 +120,7 @@ class AppLifeCycleNotifier extends StateNotifier { if (syncSuccess) { await Future.wait([ _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { - _resumeBackup(); + unawaited(_resumeBackup()); }), _resumeBackup(), // TODO: Bring back when the soft freeze issue is addressed diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index 6808860ffcf74..7b1d5d2caaa04 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -88,7 +88,7 @@ class AssetViewerStateNotifier extends Notifier { } void reset() { - _assetSubscription?.cancel(); + unawaited(_assetSubscription?.cancel()); _assetSubscription = null; state = const AssetViewerState(); } @@ -102,7 +102,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/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart index 51119f4ba2ddd..47d67c9674e7e 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'; @@ -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 463a1ac3d2e7d..74d697a2eae8b 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/backup/backup_album.provider.dart b/mobile/lib/providers/backup/backup_album.provider.dart index f81f905c2fa27..25a4204928c84 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/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index 943ac930ecbf2..776888146bc94 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,7 +53,7 @@ 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 { diff --git a/mobile/lib/providers/cleanup.provider.dart b/mobile/lib/providers/cleanup.provider.dart index 378ceb010f15f..4316b4eb00c0c 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 315c67a2146ca..6d4703c834686 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,7 +10,7 @@ class GalleryPermissionNotifier extends StateNotifier { : super(PermissionStatus.denied) // Denied is the initial state { // Sets the initial state - getGalleryPermissionStatus(); + unawaited(getGalleryPermissionStatus()); } bool get hasPermission => state.isGranted || state.isLimited; diff --git a/mobile/lib/providers/haptic_feedback.provider.dart b/mobile/lib/providers/haptic_feedback.provider.dart index e1ce5c8d0d41a..850935163ded1 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'; @@ -15,31 +17,31 @@ class HapticNotifier extends StateNotifier { void selectionClick() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.selectionClick(); + unawaited(HapticFeedback.selectionClick()); } } void lightImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.lightImpact(); + unawaited(HapticFeedback.lightImpact()); } } void mediumImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.mediumImpact(); + unawaited(HapticFeedback.mediumImpact()); } } void heavyImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.heavyImpact(); + unawaited(HapticFeedback.heavyImpact()); } } 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 7e08686078071..d4cd39bbd3e65 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -137,28 +137,6 @@ class ActionNotifier extends Notifier { } } - 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 { diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart index d503919c90564..be94a8a341d99 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/toast.provider.dart b/mobile/lib/providers/infrastructure/toast.provider.dart index 27d1cf9e6b904..eaaffd6fca639 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 d2860975bb72d..58bd4fb0f836e 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 { diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index b643264dca687..12e9e59fac1f5 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/permission.provider.dart b/mobile/lib/providers/permission.provider.dart index b7011e1357f84..dc822851227f5 100644 --- a/mobile/lib/providers/permission.provider.dart +++ b/mobile/lib/providers/permission.provider.dart @@ -10,7 +10,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/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index bf83b36f5469b..c25e496a046ef 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'; @@ -77,7 +79,7 @@ class ServerInfoNotifier extends StateNotifier { void handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) { // Update local server version - _checkServerVersionMismatch(serverVersion, latestVersion: latestVersion); + unawaited(_checkServerVersionMismatch(serverVersion, latestVersion: latestVersion)); } Future getServerFeatures() async { diff --git a/mobile/lib/providers/shared_link.provider.dart b/mobile/lib/providers/shared_link.provider.dart index fb44aea203c25..096919f28a71f 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/user.provider.dart b/mobile/lib/providers/user.provider.dart index 622847b0c24a5..427a1bf1c783e 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -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 a7c08457af039..2eb8ddc2b4741 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -143,8 +143,8 @@ 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()); } void _handleReleaseUpdates(dynamic data) { @@ -203,7 +203,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 +224,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 2024b75c6eddc..9b92b491bff65 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 5bfb18a00fcc6..6058883544b89 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -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/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index e0d4cc4632eb2..8c21af65993aa 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()), ), ), @@ -88,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/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index 57304af44f65f..f126788008622 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -13,10 +13,12 @@ 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; - }); + 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; + }), + ); } } diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 19782c8512f5d..dd1b3e849632e 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -68,16 +68,6 @@ class ActionService { 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); @@ -157,6 +147,10 @@ class ActionService { } } + if (!context.mounted) { + return false; + } + final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); if (location == null) { @@ -195,6 +189,10 @@ class ActionService { initialDate = dt; } + if (!context.mounted) { + return false; + } + final dateTime = await showDateTimePicker( context: context, initialDateTime: initialDate, diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 5b379ff890be9..5312107f6cdf1 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 diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index de8e8af3f54fe..f38b20cc21a68 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'; diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 7c0352a00e04a..36d3975a26bda 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -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/immich_logger.service.dart b/mobile/lib/services/immich_logger.service.dart index fab4b9966a80b..bfc9bd9b51708 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 5b50e8a8901b0..b439af866868d 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/share_intent_service.dart b/mobile/lib/services/share_intent_service.dart index fca5c4a188446..a5ab5d8bc9398 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 0cca50fdeca26..2b61a945ff746 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 22a3407e5ad3d..e822d1ebb66ec 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/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 0e5a3123e7250..4219e0aed7434 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -192,7 +192,7 @@ enum ActionButtonType { bool menuItem = false, ]) { return switch (this) { - ActionButtonType.advancedInfo => ActionMenuItemWidget(action: AssetDebugAction(assets: [context.asset])), + ActionButtonType.advancedInfo => ActionMenuItem(action: AssetDebugAction(source: context.source)), ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.shareLink => ShareLinkActionButton( source: context.source, diff --git a/mobile/lib/utils/async_mutex.dart b/mobile/lib/utils/async_mutex.dart index b97ab9b052840..6c54c1220db29 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/hooks/app_settings_update_hook.dart b/mobile/lib/utils/hooks/app_settings_update_hook.dart index c498b60b06870..9d7cc4b06456c 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/image_converter.dart b/mobile/lib/utils/image_converter.dart index d0fd4f873f9a4..1a3a00130ae75 100644 --- a/mobile/lib/utils/image_converter.dart +++ b/mobile/lib/utils/image_converter.dart @@ -15,13 +15,15 @@ Future imageToUint8List(Image image) async { .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/map_utils.dart b/mobile/lib/utils/map_utils.dart index 19c66e51e93a1..d5e6f957a55ec 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 { - final 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(), diff --git a/mobile/lib/widgets/activities/comment_bubble.dart b/mobile/lib/widgets/activities/comment_bubble.dart index 95cff7b87d491..fcaff8bfc3f88 100644 --- a/mobile/lib/widgets/activities/comment_bubble.dart +++ b/mobile/lib/widgets/activities/comment_bubble.dart @@ -35,6 +35,10 @@ class CommentBubble extends ConsumerWidget { 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/asset_viewer/animated_play_pause.dart b/mobile/lib/widgets/asset_viewer/animated_play_pause.dart index 4be7f49b5af58..bad8a6345c733 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/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart index 85f655ec86711..999b64e9de67d 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'; @@ -54,7 +56,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { ref.watch(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 22c860becf8a5..53c1eb1af9b4d 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 @@ -38,8 +38,8 @@ class ImmichAppBarDialog extends HookConsumerWidget { 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; }, []); @@ -126,6 +126,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); ref.read(websocketProvider.notifier).disconnect(); + if (!context.mounted) { + return; + } + unawaited(context.replaceRoute(const LoginRoute())); }, ); @@ -180,7 +184,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 +192,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 +203,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( 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 fbec03bbbdfe6..348e3aa14ceea 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; @@ -31,7 +33,7 @@ class AppBarServerInfo extends HookConsumerWidget { } 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 c29475351e52c..806ac87c19778 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'; @@ -20,7 +21,7 @@ class ServerUpdateNotification extends HookConsumerWidget { final Color infoColor = context.isDarkTheme ? context.primaryColor.withAlpha(55) : context.primaryColor.withAlpha(25); - void openUpdateLink() { + Future openUpdateLink() { String url; if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) { url = kImmichLatestRelease; @@ -35,7 +36,7 @@ class ServerUpdateNotification extends HookConsumerWidget { } } - launchUrlString(url, mode: LaunchMode.externalApplication); + return launchUrlString(url, mode: LaunchMode.externalApplication); } return SizedBox( @@ -68,7 +69,7 @@ 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: Size.zero, diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 679241fc1b8fd..3aedd55de5c11 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -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 bf0c75c8aa539..54568c392ad71 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 52f957f7e7e10..1fdc5213dfa51 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 22528b05d5448..c5bce92cff01a 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'; @@ -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/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index 90dfd9c82ae28..0ceb98aa76a41 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 80dded2130756..0e6829243c7e2 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -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 4d2dc5ef885e1..09a0367039ed9 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -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/tag_picker.dart b/mobile/lib/widgets/common/tag_picker.dart index 97fbff1930713..a9a68fe0441c7 100644 --- a/mobile/lib/widgets/common/tag_picker.dart +++ b/mobile/lib/widgets/common/tag_picker.dart @@ -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/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 7ab556b2925a8..dd307f4ede45d 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -59,26 +59,29 @@ class ChangePasswordForm extends HookConsumerWidget { .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; + } + + await ref.read(authProvider.notifier).logout(); + ref.read(websocketProvider.notifier).disconnect(); + if (!context.mounted) { + return; } + + AutoRouter.of(context).back(); + ImmichToast.show( + context: context, + msg: "login_password_changed_success".tr(), + toastType: ToastType.success, + gravity: ToastGravity.TOP, + ); } }, ), diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 4c9b56646f158..18963f0cfd181 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -70,7 +70,8 @@ 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(); @@ -114,6 +115,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 +128,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 +141,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(), @@ -179,6 +192,10 @@ class LoginForm extends HookConsumerWidget { Future getManageMediaPermission() async { final hasPermission = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); + if (!context.mounted) { + return; + } + if (!hasPermission) { await showDialog( context: context, @@ -235,6 +252,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 { @@ -245,10 +266,18 @@ class LoginForm extends HookConsumerWidget { unawaited(handleSyncFlow()); 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(), @@ -303,6 +332,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(), @@ -333,12 +366,20 @@ class LoginForm extends HookConsumerWidget { } unawaited(handleSyncFlow()); unawaited(ref.read(featureMessageServiceProvider).markSeen()); + if (!context.mounted) { + return; + } + unawaited(context.router.replaceAll([const TabShellRoute()])); return; } } 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 +388,10 @@ class LoginForm extends HookConsumerWidget { ); } finally {} } else { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: "login_form_failed_get_oauth_server_disable".tr(), @@ -358,7 +403,7 @@ class LoginForm extends HookConsumerWidget { } SingleChildRenderObjectWidget buildVersionCompatWarning() { - checkVersionMismatch(); + unawaited(checkVersionMismatch()); if (warningMessage.value == null) { return const SizedBox.shrink(); diff --git a/mobile/lib/widgets/forms/pin_registration_form.dart b/mobile/lib/widgets/forms/pin_registration_form.dart index b3270ec5249fe..ebd299bcced0b 100644 --- a/mobile/lib/widgets/forms/pin_registration_form.dart +++ b/mobile/lib/widgets/forms/pin_registration_form.dart @@ -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/map/map_settings/map_theme_picker.dart b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart index 7866c0ecdcf3f..e66f08c221b88 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/photo_view/src/controller/photo_view_controller.dart b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart index b9475a9ee252e..08c300e9aed43 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) { 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 db66cb962df39..e55df571059d4 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -82,7 +82,6 @@ class _ImageWrapperState extends State { ImageStreamListener? _imageStreamListener; ImageStream? _imageStream; ImageChunkEvent? _loadingProgress; - ImageInfo? _imageInfo; bool _loading = true; Size? _imageSize; Object? _lastException; @@ -138,7 +137,6 @@ class _ImageWrapperState extends State { 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/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 1c1d42639f24e..bf8673ccc500a 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'; @@ -35,13 +36,16 @@ class AdvancedSettings extends HookConsumerWidget { final preferRemote = useState(ref.read(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) { @@ -54,12 +58,12 @@ class AdvancedSettings extends HookConsumerWidget { } useEffect(() { - () async { + unawaited(() async { isManageMediaSupported.value = await checkAndroidVersion(); if (isManageMediaSupported.value) { 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 f915df04f8ade..eda0d819d5f9d 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'; @@ -15,7 +17,7 @@ class LayoutSettings extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tilesPerRow = useState(ref.read(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 3ac72d661207f..1f3f4bbcf0f75 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/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index f65af6af9ddf5..e3173dcdc7f63 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'; @@ -14,7 +16,7 @@ class ImageViewerQualitySetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isOriginal = useState(ref.read(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 730521e3c167f..c785e8fed4187 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'; @@ -13,7 +15,7 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tapToNavigate = useState(ref.read(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 af361943ec8d3..ec52d23ca80c0 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'; @@ -22,16 +24,16 @@ class SlideshowSettings extends HookConsumerWidget { 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 81929d95b9d88..2b302e042708b 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'; @@ -17,13 +19,13 @@ class VideoViewerSettings extends HookConsumerWidget { 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 44a1a1d7a9db8..f0f6435f90d0e 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -232,7 +232,7 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { 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 92787077a1a12..58a044cea4523 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) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database exported successfully".t(context: context))), - ); + if (!context.mounted) { + return; } + + 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)) @@ -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 7b16c2d67daa7..72e85ac8211e0 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,9 +32,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState { @override void initState() { super.initState(); - WakelockPlus.enable(); + unawaited(WakelockPlus.enable()); WidgetsBinding.instance.addPostFrameCallback((_) { - _initializeAlbumDefaults(); + unawaited(_initializeAlbumDefaults()); }); } @@ -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) { @@ -169,13 +171,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 void dispose() { super.dispose(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); } @override @@ -773,7 +775,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 2482801923a5b..b12d1476f5921 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -21,6 +21,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 { diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index e8310caed4829..162f1252a8c9e 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 f3c2b6c97fd5a..68278e0e1d464 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'; @@ -44,13 +46,13 @@ class ExternalNetworkPreference extends HookConsumerWidget { entries.value.insert(newIndex, entry); entries.value = [...entries.value]; - saveEndpointList(); + unawaited(saveEndpointList()); } 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 f8b6b087a3c1b..8b787bcaea85b 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -96,6 +96,9 @@ class LocalNetworkPreference extends HookConsumerWidget { 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 7e6e169de7c4e..513648f030ca9 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'; @@ -21,7 +23,7 @@ class NetworkingSettings extends HookConsumerWidget { final currentEndpoint = getServerUrl(); final featureEnabled = useState(ref.read(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 ee2e15f52b829..8c858a231bf43 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'; @@ -17,20 +19,22 @@ class NotificationSetting extends HookConsumerWidget { void openAppNotificationSettings(BuildContext ctx) { ctx.pop(); - openAppSettings(); + unawaited(openAppSettings()); } // When permissions are permanently denied, you need to go to settings to // allow them void 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()), - ], + 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()), + ], + ), ), ); } 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 3fead2c59f24a..b1564b414505e 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'; @@ -21,21 +23,25 @@ class PrimaryColorSetting extends HookConsumerWidget { void popBottomSheet() { Future.delayed(const Duration(milliseconds: 200), () { + if (!context.mounted) { + return; + } + Navigator.pop(context); }); } void onUseSystemColorChange(bool newValue) { - ref.read(settingsProvider).write(.themeDynamic, newValue); + unawaited(ref.read(settingsProvider).write(.themeDynamic, newValue)); popBottomSheet(); } void onPrimaryColorChange(ImmichColorPreset colorPreset) { - ref.read(settingsProvider).write(.themePrimaryColor, 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(); } @@ -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), diff --git a/mobile/lib/widgets/settings/preference_settings/share_setting.dart b/mobile/lib/widgets/settings/preference_settings/share_setting.dart index 2435810566da4..1881703023867 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 ffeeceae027e4..ec84d7be013bd 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'; @@ -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/wm_executor.dart b/mobile/lib/wm_executor.dart index e873c5f76d1e7..aac6d3e1742f9 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/packages/ui/test/formatted_text_test.dart b/mobile/packages/ui/test/formatted_text_test.dart index c3901cd802ba5..5f26822855e6a 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/test/presentation/widgets/timeline/timeline_args_test.dart b/mobile/test/presentation/widgets/timeline/timeline_args_test.dart index 0828e8e989a5e..5c03998bfcedf 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/repository.mocks.dart b/mobile/test/repository.mocks.dart index 82c9395b58a6d..b56a8a098acbb 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/service.mocks.dart b/mobile/test/service.mocks.dart index 300c54dcbb5fa..785567de562b0 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -12,6 +12,7 @@ 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/server_info.service.dart'; +import 'package:immich_mobile/services/toast.service.dart'; import 'package:mocktail/mocktail.dart'; class MockApiService extends Mock implements ApiService {} @@ -43,3 +44,5 @@ class MockServerInfoService extends Mock implements ServerInfoService {} class MockCleanupService extends Mock implements CleanupService {} class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} + +class MockToastService extends Mock implements ToastService {} diff --git a/mobile/test/services/deep_link_service_test.dart b/mobile/test/services/deep_link_service_test.dart index ff090367ea02c..16fa392f272dc 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/view_intent_service_test.dart b/mobile/test/services/view_intent_service_test.dart index 7b3d0b85e7af8..fd8f5f725c012 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 d8eadda7ae529..7dd15eb9a341c 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(); @@ -115,6 +113,7 @@ class ServiceMocks { final upload = MockForegroundUploadService(); final cast = MockGCastService(); final serverInfo = MockServerInfoService(); + final toast = MockToastService(); ServiceMocks() { resetAll(); @@ -132,6 +131,7 @@ class ServiceMocks { reset(serverInfo); reset(backgroundSync); reset(upload); + reset(toast); _stubUserService(); _stubPartnerService(); _stubAssetService(); 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 4e84c100cb282..1644df039681f 100644 --- a/mobile/test/unit/presentation/actions/asset_debug_action_test.dart +++ b/mobile/test/unit/presentation/actions/asset_debug_action_test.dart @@ -24,7 +24,8 @@ void main() { 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/favorite_action_test.dart b/mobile/test/unit/presentation/actions/favorite_action_test.dart index 722d9d1dc7194..cb4d6130abedb 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'; @@ -24,55 +27,66 @@ void main() { 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])); - - 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])); + await pumpFavorite(tester, {mine, theirs}); - 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/timeline_action_test.dart b/mobile/test/unit/presentation/actions/timeline_action_test.dart deleted file mode 100644 index 5661be72c0a05..0000000000000 --- 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/partner_page_test.dart b/mobile/test/unit/presentation/partner_page_test.dart index ee9c6a3575961..e6ff8fc83f29a 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'; @@ -17,12 +19,10 @@ void main() { group('PartnerSharedByList', () { testWidgets('shows the empty-state add button when there are no partners', (tester) async { - const action = 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(); - const action = 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 25de5830498e0..a45c1c14f9179 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -5,6 +5,7 @@ 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'; @@ -16,6 +17,7 @@ import 'package:immich_mobile/presentation/actions/action.widget.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/services/gcast.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; @@ -52,6 +54,12 @@ class PresentationContext { inLockedViewProvider.overrideWithValue(false), ]; + List selected(Set assets) => [ + multiSelectProvider.overrideWith( + () => MultiSelectNotifier(MultiSelectState(selectedAssets: assets, lockedSelectionAssets: const {})), + ), + ]; + static Future create() async { TestUtils.init(); if (_db == null) { @@ -103,10 +111,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/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index dd5d4dce45250..bc3bf82094a27 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/packages/cli/Dockerfile b/packages/cli/Dockerfile index ee2a4294bd77c..716522ecd7b5b 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 28d5e1858fbb4..320ce001d8bef 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 3a49307316694..bfc939c084899 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/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index a7e0d9511e8c3..3ac958ce2d962 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/server/Dockerfile b/server/Dockerfile index b7a4e105a3d2f..df9d8ad7bacce 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -60,12 +60,13 @@ COPY --from=ghcr.io/jdx/mise:2026.7.15@sha256:e62097049bfc980de5d9a25fbe16431e24 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/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts index 3fe9b493686aa..eca7e5bcb5b2b 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 d105dd90b9cb6..3bf1d59f8bb1e 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 63cdce4f322c5..b96ba4dee33ee 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 0000000000000..43fa779e8de9a --- /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 737c8f395858c..4cb2092d1b190 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 67962e8b3c40c..3d39a4dd3d306 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 d5f13e341cfce..3d8c6c1194ed8 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 7f2313a05811a..54f5c1f10bdfb 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 f5ce4b851c45b..6407155492ee4 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 3c871f672d10c..1e3b3b6193e44 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 0000000000000..5b094c03c1304 --- /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 d9870ec7b9690..93bcfe26e76b7 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/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts b/server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts new file mode 100644 index 0000000000000..82f742c90ad9d --- /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 0000000000000..3be799f49835f --- /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 f54658be6540b..c0d13d4902244 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 0839924d2a0c5..50d56d9067fa3 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/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index dad13ef5f5915..48dcf5a50936c 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 f3be40f7dd705..59e20276af1e5 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 6d17410441d79..2195c66a22761 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 e598f1c71de81..1eaefb689d0bd 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 ca1e05d93fa13..bcd6f2e834c9c 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/server.service.spec.ts b/server/src/services/server.service.spec.ts index e1575a496a19d..b4b1af35d6527 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 ad212292ba870..57342f9509814 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 87fc40306fb12..e3842b1503c81 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/test/medium/responses.ts b/server/test/medium/responses.ts index b416b3b90412e..9b75bc85eca12 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 1fc306f790578..5e5c880955b68 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/sync/sync-album-to-asset.spec.ts b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts index a0802abe731cc..0fd58c527cff3 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/utils.ts b/server/test/utils.ts index b633cbc4de072..2f32f3b41211f 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 b0d41317cb3ba..7b9e3c2f3b064 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/src/lib/modals/AlbumEditModal.svelte b/web/src/lib/modals/AlbumEditModal.svelte index 7c4ee9b6267cc..c5386a6a0d7c9 100644 --- a/web/src/lib/modals/AlbumEditModal.svelte +++ b/web/src/lib/modals/AlbumEditModal.svelte @@ -17,7 +17,7 @@ let description = $state(album.description); const onSubmit = async () => { - const success = await handleUpdateAlbum(album, { albumName, description }); + const success = await handleUpdateAlbum(album, { albumName, description: description || null }); if (success) { onClose(); } 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 1d80fcb766091..cb2fb2a9bb165 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);