mirror of
https://github.com/immich-app/immich.git
synced 2026-07-06 12:36:55 -07:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3beaeba72 | |||
| d0b779d51e | |||
| 61210eab28 | |||
| b6cb534ea6 | |||
| 4e25db2989 | |||
| 4f5e0d3505 | |||
| b3abe0bb5d | |||
| c04acc8951 | |||
| af32a8ca7c | |||
| ed83fb3f5a | |||
| 628de417e9 | |||
| be02c00afd | |||
| 43abb9a92f |
@@ -103,14 +103,14 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||
|
||||
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Restore Gradle Cache
|
||||
id: cache-gradle-restore
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
|
||||
- name: Save Gradle Cache
|
||||
id: cache-gradle-save
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
run: flutter build ios --config-only --no-codesign
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
|
||||
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@e24529087d93f837b28b50bb66ba9016380a7fcc # v0.1.2
|
||||
uses: oasdiff/oasdiff-action/breaking@ae5ef6402ebe218ac6e872c59d0fa4557694e75c # v0.1.4
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ needs.bump_version.outputs.rc }}
|
||||
|
||||
@@ -7,30 +7,37 @@ 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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
version = "1.0.3"
|
||||
@@ -39,27 +46,34 @@ backend = "aqua:gruntwork-io/terragrunt"
|
||||
[tools.terragrunt."platforms.linux-arm64"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
||||
|
||||
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
||||
|
||||
[tools.terragrunt."platforms.macos-arm64"]
|
||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654084"
|
||||
|
||||
[tools.terragrunt."platforms.macos-x64"]
|
||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406653970"
|
||||
|
||||
[tools.terragrunt."platforms.windows-x64"]
|
||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654412"
|
||||
|
||||
@@ -2150,6 +2150,7 @@
|
||||
"unknown": "Unknown",
|
||||
"unknown_country": "Unknown Country",
|
||||
"unknown_date": "Unknown date",
|
||||
"unknown_schema": "Unknown schema",
|
||||
"unknown_year": "Unknown Year",
|
||||
"unlimited": "Unlimited",
|
||||
"unlink_motion_video": "Unlink motion video",
|
||||
|
||||
+21
-14
@@ -5,38 +5,38 @@ version = "3.11.15"
|
||||
backend = "core:python"
|
||||
|
||||
[tools.python."platforms.linux-arm64"]
|
||||
checksum = "sha256:cbce0660e88cd9c56be7aaf2a2df92bea51f359388a521838b6b01817d728df0"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:9ac18c9a761e91e6c6452bc0ef0082922a00a3fdec734555635d57c3169309b7"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:a55ea44225ee3741d4157c383f3d5c3e8eee5f9665e2ea069233486b4275d928"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:bcd414f2e9d8cc5d4ac7fab2c8f912e30379c150fa6b94da19d64daf85cfc5d4"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.linux-x64"]
|
||||
checksum = "sha256:67a5b22f796e96f4d7fa628f95866d5fd1d524d0588f74e4601facd82b66792b"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:0604cd029b142dc223e131f17f5941c0c8d2d5074997c8178b515b19eea2a6c2"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:5a8544aa4303da3ca4b7505c98dd8453b671157039d25cd551e55abea0f83a60"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:45bffdb320cf4a2c2dbf183b634aeb7e28f9cb0646f715b2f4117c11084d839a"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.macos-arm64"]
|
||||
checksum = "sha256:8c56f1f59142e0f9f8861ad897bdfd97fd84403afa7b3d8b0f33b208ec471355"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:2318799eaf104f8a29bc09a93b0851b05dbbcb4ce9a5f045ddea169c0c7ff3a5"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.macos-x64"]
|
||||
checksum = "sha256:8cd3878c656ba1698314cbcb65f78df4c37b7c8eabff958558115c6db11adb3d"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:4925e5aaa9bc77c85302d350b36c1d9def2002996a6bcfa55c88ba6eb318de29"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.windows-x64"]
|
||||
checksum = "sha256:f081a733b4e7ba0e5e5e12d533b3c795dbef3ecbebf92f0b4202e5329bf7c8ab"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:6589ca6d63f520bec4096d62b3ab91da3d0a80b16b594c99a6b677e335814683"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.uv]]
|
||||
@@ -46,27 +46,34 @@ backend = "aqua:astral-sh/uv"
|
||||
[tools.uv."platforms.linux-arm64"]
|
||||
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235419"
|
||||
|
||||
[tools.uv."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235419"
|
||||
|
||||
[tools.uv."platforms.linux-x64"]
|
||||
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235475"
|
||||
|
||||
[tools.uv."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235475"
|
||||
|
||||
[tools.uv."platforms.macos-arm64"]
|
||||
checksum = "sha256:103367962c5cb00bf7370d84cbaa3fec5a9807be9cc833ea9d8eea400c119fa2"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-apple-darwin.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235412"
|
||||
|
||||
[tools.uv."platforms.macos-x64"]
|
||||
checksum = "sha256:2bbef70982e97dfc36454de173f35ec1a5e83ae11e3885df6a50db3fd76171cb"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-apple-darwin.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235464"
|
||||
|
||||
[tools.uv."platforms.windows-x64"]
|
||||
checksum = "sha256:459d95892a5cc5c21779532f4f41b9238594b79e312a5142da2148ecfa10e705"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-pc-windows-msvc.zip"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/289235467"
|
||||
|
||||
@@ -254,30 +254,37 @@ 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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[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"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "11.6.0"
|
||||
@@ -286,31 +293,37 @@ backend = "aqua:pnpm/pnpm"
|
||||
[tools.pnpm."platforms.linux-arm64"]
|
||||
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120174"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120177"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64"]
|
||||
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120176"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120175"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-arm64"]
|
||||
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120180"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.windows-x64"]
|
||||
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120173"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
@@ -320,27 +333,34 @@ backend = "aqua:gruntwork-io/terragrunt"
|
||||
[tools.terragrunt."platforms.linux-arm64"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
||||
|
||||
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
|
||||
|
||||
[tools.terragrunt."platforms.macos-arm64"]
|
||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654084"
|
||||
|
||||
[tools.terragrunt."platforms.macos-x64"]
|
||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406653970"
|
||||
|
||||
[tools.terragrunt."platforms.windows-x64"]
|
||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654412"
|
||||
|
||||
@@ -1,19 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum AspectRatioPreset {
|
||||
free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
|
||||
square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
|
||||
ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded),
|
||||
ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded),
|
||||
ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded),
|
||||
ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true),
|
||||
ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true),
|
||||
ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true);
|
||||
class CropAspectRatio {
|
||||
final int? numerator;
|
||||
final int? denominator;
|
||||
|
||||
final double? ratio;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final bool iconRotated;
|
||||
final String? customLabel;
|
||||
final IconData? icon;
|
||||
|
||||
const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
|
||||
const CropAspectRatio({this.numerator, this.denominator, this.customLabel, this.icon});
|
||||
|
||||
static const free = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||
static const original = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||
|
||||
String get label {
|
||||
return customLabel ?? (numerator != null && denominator != null ? '$numerator:$denominator' : 'Free');
|
||||
}
|
||||
|
||||
bool get hasFlippedVariant => numerator != denominator;
|
||||
double? get ratio => (numerator != null && denominator != null) ? numerator! / denominator! : null;
|
||||
|
||||
CropAspectRatio get flipped {
|
||||
return CropAspectRatio(numerator: denominator, denominator: numerator, customLabel: customLabel, icon: icon);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return other is CropAspectRatio &&
|
||||
other.numerator == numerator &&
|
||||
other.denominator == denominator &&
|
||||
other.customLabel == customLabel &&
|
||||
other.icon == icon;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return numerator.hashCode ^ denominator.hashCode ^ customLabel.hashCode ^ icon.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
const aspectRatioFree = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||
const aspectRatioOriginal = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||
|
||||
final aspectRatioPresets = [
|
||||
CropAspectRatio.free,
|
||||
CropAspectRatio.original,
|
||||
|
||||
const CropAspectRatio(numerator: 1, denominator: 1),
|
||||
|
||||
// lanscape
|
||||
const CropAspectRatio(numerator: 16, denominator: 9),
|
||||
const CropAspectRatio(numerator: 3, denominator: 2),
|
||||
const CropAspectRatio(numerator: 7, denominator: 5),
|
||||
const CropAspectRatio(numerator: 4, denominator: 3),
|
||||
|
||||
// portrait
|
||||
const CropAspectRatio(numerator: 16, denominator: 9).flipped,
|
||||
const CropAspectRatio(numerator: 3, denominator: 2).flipped,
|
||||
const CropAspectRatio(numerator: 7, denominator: 5).flipped,
|
||||
const CropAspectRatio(numerator: 4, denominator: 3).flipped,
|
||||
];
|
||||
|
||||
@@ -15,8 +15,6 @@ enum ActionSource { timeline, viewer }
|
||||
|
||||
enum ShareAssetType { original, preview }
|
||||
|
||||
enum ButtonPosition { bottomBar, kebabMenu }
|
||||
|
||||
enum CleanupStep { selectDate, scan, delete }
|
||||
|
||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||
|
||||
@@ -154,7 +154,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
||||
}
|
||||
|
||||
class _AspectRatioButton extends StatelessWidget {
|
||||
final AspectRatioPreset ratio;
|
||||
final CropAspectRatio ratio;
|
||||
final bool isSelected;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@@ -162,15 +162,16 @@ class _AspectRatioButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = isSelected ? context.primaryColor : context.themeData.iconTheme.color;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 36,
|
||||
icon: Transform.rotate(
|
||||
angle: ratio.iconRotated ? pi / 2 : 0,
|
||||
child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color),
|
||||
),
|
||||
icon: ratio.ratio != null
|
||||
? _AspectRatioRect(ratio: ratio.ratio!, color: color)
|
||||
: Icon(ratio.icon, color: color),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
Text(ratio.label, style: context.textTheme.displayMedium),
|
||||
@@ -179,6 +180,32 @@ class _AspectRatioButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AspectRatioRect extends StatelessWidget {
|
||||
final double ratio;
|
||||
final Color? color;
|
||||
|
||||
const _AspectRatioRect({required this.ratio, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: color ?? Colors.transparent, width: 3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AspectRatioSelector extends ConsumerWidget {
|
||||
const _AspectRatioSelector();
|
||||
|
||||
@@ -187,22 +214,16 @@ class _AspectRatioSelector extends ConsumerWidget {
|
||||
final editorState = ref.watch(editorStateProvider);
|
||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||
|
||||
// the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees
|
||||
double? selectedAspectRatio = editorState.aspectRatio;
|
||||
if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) {
|
||||
selectedAspectRatio = 1 / selectedAspectRatio;
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: AspectRatioPreset.values.map((entry) {
|
||||
children: aspectRatioPresets.map((entry) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _AspectRatioButton(
|
||||
ratio: entry,
|
||||
isSelected: selectedAspectRatio == entry.ratio,
|
||||
onPressed: () => editorNotifier.setAspectRatio(entry.ratio),
|
||||
isSelected: editorState.aspectRatio == entry,
|
||||
onPressed: () => editorNotifier.setAspectRatio(entry),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -357,8 +378,24 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
||||
final editorState = ref.watch(editorStateProvider);
|
||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||
|
||||
ref.listen(editorStateProvider, (_, current) {
|
||||
cropController.aspectRatio = current.aspectRatio;
|
||||
ref.listen(editorStateProvider, (previous, current) {
|
||||
// Only re-apply the aspect ratio when it changes, otherwise the crop rect will shrink on every rotation
|
||||
if (previous?.aspectRatio != current.aspectRatio) {
|
||||
double? ratio;
|
||||
|
||||
switch (current.aspectRatio) {
|
||||
case CropAspectRatio.original:
|
||||
ratio = current.originalWidth / current.originalHeight;
|
||||
default:
|
||||
ratio = current.aspectRatio.ratio;
|
||||
}
|
||||
|
||||
if (current.rotationAngle % 180 != 0) {
|
||||
ratio = ratio != null ? 1 / ratio : null;
|
||||
}
|
||||
|
||||
cropController.aspectRatio = ratio;
|
||||
}
|
||||
|
||||
if (cropController.crop != current.crop) {
|
||||
cropController.crop = current.crop;
|
||||
@@ -386,7 +423,9 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
||||
1.0,
|
||||
1.0,
|
||||
),
|
||||
child: Container(
|
||||
child: AnimatedContainer(
|
||||
duration: editorState.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
padding: const EdgeInsets.all(10),
|
||||
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
||||
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/aspect_ratios.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/utils/editor.utils.dart';
|
||||
@@ -60,13 +61,8 @@ class EditorProvider extends Notifier<EditorState> {
|
||||
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
||||
}
|
||||
|
||||
void setAspectRatio(double? aspectRatio) {
|
||||
if (aspectRatio != null && state.rotationAngle % 180 != 0) {
|
||||
// When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations
|
||||
aspectRatio = 1 / aspectRatio;
|
||||
}
|
||||
|
||||
state = state.copyWith(aspectRatio: aspectRatio);
|
||||
void setAspectRatio(CropAspectRatio preset) {
|
||||
state = state.copyWith(aspectRatio: preset, hasUnsavedEdits: true);
|
||||
}
|
||||
|
||||
void resetEdits() {
|
||||
@@ -76,19 +72,19 @@ class EditorProvider extends Notifier<EditorState> {
|
||||
flipHorizontal: false,
|
||||
flipVertical: false,
|
||||
crop: const Rect.fromLTRB(0, 0, 1, 1),
|
||||
aspectRatio: null,
|
||||
aspectRatio: CropAspectRatio.free,
|
||||
hasUnsavedEdits: true,
|
||||
);
|
||||
}
|
||||
|
||||
void rotateCCW() {
|
||||
_animateRotation(state.rotationAngle - 90);
|
||||
state = state.copyWith(hasUnsavedEdits: true);
|
||||
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||
}
|
||||
|
||||
void rotateCW() {
|
||||
_animateRotation(state.rotationAngle + 90);
|
||||
state = state.copyWith(hasUnsavedEdits: true);
|
||||
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||
}
|
||||
|
||||
void flipHorizontally() {
|
||||
@@ -117,7 +113,7 @@ class EditorState {
|
||||
final bool flipHorizontal;
|
||||
final bool flipVertical;
|
||||
final Rect crop;
|
||||
final double? aspectRatio;
|
||||
final CropAspectRatio aspectRatio;
|
||||
|
||||
final int originalWidth;
|
||||
final int originalHeight;
|
||||
@@ -132,7 +128,7 @@ class EditorState {
|
||||
bool? flipHorizontal,
|
||||
bool? flipVertical,
|
||||
Rect? crop,
|
||||
this.aspectRatio,
|
||||
CropAspectRatio? aspectRatio,
|
||||
int? originalWidth,
|
||||
int? originalHeight,
|
||||
Duration? animationDuration,
|
||||
@@ -145,6 +141,7 @@ class EditorState {
|
||||
originalWidth = originalWidth ?? 0,
|
||||
originalHeight = originalHeight ?? 0,
|
||||
crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
||||
aspectRatio = aspectRatio ?? CropAspectRatio.free,
|
||||
hasUnsavedEdits = hasUnsavedEdits ?? false;
|
||||
|
||||
EditorState copyWith({
|
||||
@@ -152,7 +149,7 @@ class EditorState {
|
||||
int? rotationAngle,
|
||||
bool? flipHorizontal,
|
||||
bool? flipVertical,
|
||||
double? aspectRatio = double.infinity,
|
||||
CropAspectRatio? aspectRatio,
|
||||
int? originalWidth,
|
||||
int? originalHeight,
|
||||
Duration? animationDuration,
|
||||
@@ -164,7 +161,7 @@ class EditorState {
|
||||
rotationAngle: rotationAngle ?? this.rotationAngle,
|
||||
flipHorizontal: flipHorizontal ?? this.flipHorizontal,
|
||||
flipVertical: flipVertical ?? this.flipVertical,
|
||||
aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio,
|
||||
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||
animationDuration: animationDuration ?? this.animationDuration,
|
||||
originalWidth: originalWidth ?? this.originalWidth,
|
||||
originalHeight: originalHeight ?? this.originalHeight,
|
||||
|
||||
@@ -15,10 +15,7 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class EditImageActionButton extends ConsumerWidget {
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const EditImageActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
const EditImageActionButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -57,8 +54,6 @@ class EditImageActionButton extends ConsumerWidget {
|
||||
iconData: Icons.tune,
|
||||
label: "edit".t(context: context),
|
||||
onPressed: onPress,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class OpenActivityActionButton extends ConsumerWidget {
|
||||
const OpenActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.read(currentRemoteAlbumProvider);
|
||||
final asset = ref.read(assetViewerProvider).currentAsset;
|
||||
if (album == null || asset == null) {
|
||||
return;
|
||||
}
|
||||
context.router.push(
|
||||
DriftActivitiesRoute(album: album, assetId: asset is RemoteAsset ? asset.id : null, assetName: asset.name),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => BaseActionButton(
|
||||
iconData: Icons.chat_outlined,
|
||||
label: "activity".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
@@ -159,8 +159,6 @@ class ShareActionButton extends ConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
if (!result.success) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
@@ -173,7 +171,6 @@ class ShareActionButton extends ConsumerWidget {
|
||||
buildContext.pop();
|
||||
});
|
||||
|
||||
// Show download progress with a "Preparing" message
|
||||
return preparingDialog;
|
||||
},
|
||||
barrierDismissible: false,
|
||||
|
||||
@@ -2,18 +2,24 @@ 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/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||
|
||||
class ViewerBottomBar extends ConsumerWidget {
|
||||
@@ -31,31 +37,35 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final album = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final isTrashEnabled = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final serverVersion = ref.watch(serverInfoProvider.select((state) => state.serverVersion));
|
||||
final serverInfo = ref.watch(serverInfoProvider);
|
||||
final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: isOwner,
|
||||
isArchived: isArchived,
|
||||
isTrashEnabled: isTrashEnabled,
|
||||
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||
isInLockedView: isInLockedView,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: timelineOrigin,
|
||||
originalTheme: originalTheme,
|
||||
serverVersion: serverVersion,
|
||||
);
|
||||
final actions = <Widget>[
|
||||
if (isInTrash && isOwner && asset.hasRemote)
|
||||
const RestoreActionButton(source: ActionSource.viewer)
|
||||
else
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
|
||||
final actions = ActionButtonBuilder.buildViewerBottomBar(buttonContext, context);
|
||||
if (!isInLockedView) ...[
|
||||
if (!isInTrash) ...[
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
// edit sync was added in 2.6.0
|
||||
if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0))
|
||||
const EditImageActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
],
|
||||
if (isOwner) ...[
|
||||
if (asset.isLocalOnly)
|
||||
const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||
else if (asset.isTrashed)
|
||||
const DeletePermanentActionButton(source: ActionSource.viewer, useShortLabel: true)
|
||||
else
|
||||
const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: Durations.short4,
|
||||
|
||||
@@ -31,7 +31,6 @@ class ViewerKebabMenu extends ConsumerWidget {
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final serverVersion = ref.watch(serverInfoProvider.select((state) => state.serverVersion));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
@@ -49,7 +48,6 @@ class ViewerKebabMenu extends ConsumerWidget {
|
||||
source: ActionSource.viewer,
|
||||
isCasting: isCasting,
|
||||
timelineOrigin: timelineOrigin,
|
||||
serverVersion: serverVersion,
|
||||
);
|
||||
|
||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context);
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provid
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
@@ -46,6 +47,20 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
if (album != null && album.isActivityEnabled && album.isShared)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
onPressed: () {
|
||||
context.router.push(
|
||||
DriftActivitiesRoute(
|
||||
album: album,
|
||||
assetId: asset is RemoteAsset ? asset.id : null,
|
||||
assetName: asset.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
|
||||
|
||||
ViewerKebabMenu(originalTheme: originalTheme),
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
@@ -17,11 +16,8 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/open_activity_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
||||
@@ -51,8 +47,6 @@ class ActionButtonContext {
|
||||
final bool isCasting;
|
||||
final TimelineOrigin timelineOrigin;
|
||||
final int selectedCount;
|
||||
final ThemeData? originalTheme;
|
||||
final SemVer serverVersion;
|
||||
|
||||
const ActionButtonContext({
|
||||
required this.asset,
|
||||
@@ -67,17 +61,13 @@ class ActionButtonContext {
|
||||
this.isCasting = false,
|
||||
this.timelineOrigin = TimelineOrigin.main,
|
||||
this.selectedCount = 1,
|
||||
this.originalTheme,
|
||||
this.serverVersion = const SemVer(major: 0, minor: 0, patch: 0),
|
||||
});
|
||||
}
|
||||
|
||||
enum ActionButtonType {
|
||||
openInfo,
|
||||
openActivity,
|
||||
likeActivity,
|
||||
share,
|
||||
editImage,
|
||||
shareLink,
|
||||
cast,
|
||||
setAlbumCover,
|
||||
@@ -99,16 +89,8 @@ enum ActionButtonType {
|
||||
deleteLocal,
|
||||
deletePermanent,
|
||||
delete,
|
||||
addTo,
|
||||
advancedInfo;
|
||||
|
||||
bool _isInActivityAlbum(ActionButtonContext context) {
|
||||
return !context.isInLockedView &&
|
||||
context.currentAlbum != null &&
|
||||
context.currentAlbum!.isActivityEnabled &&
|
||||
context.currentAlbum!.isShared;
|
||||
}
|
||||
|
||||
bool shouldShow(ActionButtonContext context) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
|
||||
@@ -177,7 +159,11 @@ enum ActionButtonType {
|
||||
!context.isInLockedView && //
|
||||
context.isStacked,
|
||||
ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView,
|
||||
ActionButtonType.likeActivity => _isInActivityAlbum(context),
|
||||
ActionButtonType.likeActivity =>
|
||||
!context.isInLockedView &&
|
||||
context.currentAlbum != null &&
|
||||
context.currentAlbum!.isActivityEnabled &&
|
||||
context.currentAlbum!.isShared,
|
||||
ActionButtonType.similarPhotos =>
|
||||
!context.isInLockedView && //
|
||||
context.asset is RemoteAsset,
|
||||
@@ -195,25 +181,10 @@ enum ActionButtonType {
|
||||
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
||||
context.isOwner,
|
||||
ActionButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||
ActionButtonType.editImage =>
|
||||
!context.isInLockedView &&
|
||||
context.asset.isEditable &&
|
||||
context.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0),
|
||||
ActionButtonType.addTo =>
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasRemote,
|
||||
ActionButtonType.openActivity => _isInActivityAlbum(context),
|
||||
ActionButtonType.slideshow => true,
|
||||
};
|
||||
}
|
||||
|
||||
bool showsAt(ButtonPosition position, ActionButtonContext context) => switch (this) {
|
||||
ActionButtonType.openActivity || ActionButtonType.likeActivity =>
|
||||
position != ButtonPosition.bottomBar || context.timelineOrigin != TimelineOrigin.trash,
|
||||
ActionButtonType.editImage => position != ButtonPosition.bottomBar || context.currentAlbum?.isShared != true,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
Widget buildButton(
|
||||
ActionButtonContext context, [
|
||||
BuildContext? buildContext,
|
||||
@@ -246,14 +217,8 @@ enum ActionButtonType {
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
useShortLabel: false,
|
||||
),
|
||||
ActionButtonType.delete => DeleteActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
showConfirmation: true,
|
||||
),
|
||||
ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
@@ -319,9 +284,6 @@ enum ActionButtonType {
|
||||
},
|
||||
),
|
||||
ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.editImage => EditImageActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.addTo => AddActionButton(originalTheme: context.originalTheme),
|
||||
ActionButtonType.openActivity => OpenActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -353,62 +315,34 @@ enum ActionButtonType {
|
||||
class ActionButtonBuilder {
|
||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
||||
static const List<ActionButtonType> defaultViewerKebabMenuOrder = _actionTypes;
|
||||
static const List<ActionButtonType> _defaultViewerBottomBarOrder = [
|
||||
static const Set<ActionButtonType> defaultViewerBottomBarButtons = {
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.moveToLockFolder,
|
||||
ActionButtonType.upload,
|
||||
ActionButtonType.editImage,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.openActivity,
|
||||
ActionButtonType.likeActivity,
|
||||
ActionButtonType.removeFromLockFolder,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.unarchive,
|
||||
ActionButtonType.restoreTrash,
|
||||
ActionButtonType.deletePermanent,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.deleteLocal,
|
||||
];
|
||||
};
|
||||
|
||||
static List<Widget> build(ActionButtonContext context) {
|
||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
|
||||
static List<ActionButtonType> getViewerBottomBarTypes(ActionButtonContext context) {
|
||||
return _defaultViewerBottomBarOrder
|
||||
.where((type) => type.shouldShow(context) && type.showsAt(ButtonPosition.bottomBar, context))
|
||||
.take(4)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<ActionButtonType> getViewerKebabMenuTypes(ActionButtonContext context) {
|
||||
final inBottomBar = getViewerBottomBarTypes(context).toSet();
|
||||
return defaultViewerKebabMenuOrder
|
||||
.where(
|
||||
(type) =>
|
||||
!inBottomBar.contains(type) &&
|
||||
type.shouldShow(context) &&
|
||||
type.showsAt(ButtonPosition.kebabMenu, context),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) {
|
||||
return getViewerKebabMenuTypes(context).toKebabMenuWidgets(context, buildContext);
|
||||
}
|
||||
final visibleButtons = defaultViewerKebabMenuOrder
|
||||
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
||||
.toList();
|
||||
|
||||
static List<Widget> buildViewerBottomBar(ActionButtonContext context, BuildContext buildContext) {
|
||||
return getViewerBottomBarTypes(context).toBottomBarWidgets(context, buildContext);
|
||||
}
|
||||
}
|
||||
|
||||
extension ActionButtonTypeListExtension on List<ActionButtonType> {
|
||||
List<Widget> toKebabMenuWidgets(ActionButtonContext context, BuildContext buildContext) {
|
||||
if (isEmpty) {
|
||||
if (visibleButtons.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<Widget> result = [];
|
||||
int? lastGroup;
|
||||
|
||||
for (final type in this) {
|
||||
for (final type in visibleButtons) {
|
||||
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
|
||||
result.add(const Divider(height: 1));
|
||||
}
|
||||
@@ -418,8 +352,4 @@ extension ActionButtonTypeListExtension on List<ActionButtonType> {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Widget> toBottomBarWidgets(ActionButtonContext context, BuildContext buildContext) {
|
||||
return map((type) => type.buildButton(context, buildContext, false, false)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
|
||||
LocalAsset createLocalAsset({
|
||||
String? remoteId,
|
||||
@@ -1239,438 +1238,4 @@ void main() {
|
||||
expect(nonArchivedWidgets, isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonBuilder.getViewerBottomBarTypes', () {
|
||||
test('should return correct button types for shared album with activity', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.openActivity,
|
||||
ActionButtonType.likeActivity,
|
||||
];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('should return correct button types for local only asset', () {
|
||||
final localAsset = createLocalAsset();
|
||||
final context = ActionButtonContext(
|
||||
asset: localAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
const expectedTypes = [ActionButtonType.share, ActionButtonType.upload, ActionButtonType.deleteLocal];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('should return correct button types for locked view', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: false,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.removeFromLockFolder,
|
||||
ActionButtonType.deletePermanent,
|
||||
];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('should return correct button types for remote only asset', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.editImage,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.delete,
|
||||
];
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes, expectedTypes);
|
||||
expect(bottomBarTypes.any(kebabTypes.contains), isFalse);
|
||||
});
|
||||
|
||||
test('trashed asset (no album) keeps restore and permanent delete in bottom bar', () {
|
||||
final remoteAsset = createRemoteAsset(deletedAt: DateTime.now());
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: TimelineOrigin.trash,
|
||||
);
|
||||
|
||||
const expectedTypes = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.addTo,
|
||||
ActionButtonType.restoreTrash,
|
||||
ActionButtonType.deletePermanent,
|
||||
];
|
||||
|
||||
expect(ActionButtonBuilder.getViewerBottomBarTypes(context), expectedTypes);
|
||||
});
|
||||
|
||||
test('trashed asset in shared activity album still surfaces restore and permanent delete', () {
|
||||
final remoteAsset = createRemoteAsset(deletedAt: DateTime.now());
|
||||
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: TimelineOrigin.trash,
|
||||
);
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
|
||||
expect(bottomBarTypes, contains(ActionButtonType.restoreTrash));
|
||||
expect(bottomBarTypes, contains(ActionButtonType.deletePermanent));
|
||||
expect(bottomBarTypes, isNot(contains(ActionButtonType.openActivity)));
|
||||
expect(bottomBarTypes, isNot(contains(ActionButtonType.likeActivity)));
|
||||
});
|
||||
|
||||
test('5th bottom-bar candidate overflows into the kebab', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context);
|
||||
final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context);
|
||||
|
||||
expect(bottomBarTypes.length, 4);
|
||||
expect(bottomBarTypes, isNot(contains(ActionButtonType.delete)));
|
||||
expect(kebabTypes, contains(ActionButtonType.delete));
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.shouldShow editImage button', () {
|
||||
final editableRemote = createRemoteAsset();
|
||||
|
||||
test('should show for editable remote asset on a recent server', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: editableRemote,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show when local-only (LocalAsset is not editable)', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createLocalAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show when server version is below 2.6', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: editableRemote,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 5, patch: 9),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: editableRemote,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
serverVersion: const SemVer(major: 2, minor: 6, patch: 0),
|
||||
);
|
||||
|
||||
expect(ActionButtonType.editImage.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.shouldShow addTo button', () {
|
||||
test('should show when not locked and asset has remote', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createRemoteAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.addTo.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createRemoteAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.addTo.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show for local-only asset', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createLocalAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.addTo.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.shouldShow openActivity button', () {
|
||||
final asset = createRemoteAsset();
|
||||
|
||||
test('should show in a shared activity-enabled album', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: true, isShared: true),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show when no album', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show when album not shared', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: true, isShared: false),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show when activity disabled', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: false, isShared: true),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: createRemoteAlbum(isActivityEnabled: true, isShared: true),
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.openActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.showsAt', () {
|
||||
ActionButtonContext ctx({RemoteAlbum? album, TimelineOrigin origin = TimelineOrigin.main}) => ActionButtonContext(
|
||||
asset: createRemoteAsset(),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
timelineOrigin: origin,
|
||||
);
|
||||
|
||||
group('editImage', () {
|
||||
test('hidden from bottom bar when album is shared', () {
|
||||
final context = ctx(album: createRemoteAlbum(isShared: true));
|
||||
expect(ActionButtonType.editImage.showsAt(ButtonPosition.bottomBar, context), isFalse);
|
||||
});
|
||||
|
||||
test('shown in bottom bar when album is not shared', () {
|
||||
final context = ctx(album: createRemoteAlbum(isShared: false));
|
||||
expect(ActionButtonType.editImage.showsAt(ButtonPosition.bottomBar, context), isTrue);
|
||||
});
|
||||
|
||||
test('shown in bottom bar when there is no album', () {
|
||||
expect(ActionButtonType.editImage.showsAt(ButtonPosition.bottomBar, ctx()), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('openActivity / likeActivity', () {
|
||||
test('hidden from bottom bar when viewing from trash', () {
|
||||
final context = ctx(origin: TimelineOrigin.trash);
|
||||
expect(ActionButtonType.openActivity.showsAt(ButtonPosition.bottomBar, context), isFalse);
|
||||
expect(ActionButtonType.likeActivity.showsAt(ButtonPosition.bottomBar, context), isFalse);
|
||||
});
|
||||
|
||||
test('shown in bottom bar outside trash', () {
|
||||
expect(ActionButtonType.openActivity.showsAt(ButtonPosition.bottomBar, ctx()), isTrue);
|
||||
expect(ActionButtonType.likeActivity.showsAt(ButtonPosition.bottomBar, ctx()), isTrue);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -56,7 +56,7 @@ FROM builder AS plugins
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.6.10@sha256:f57ac375a262f52f8ac3f9101348dbff2187d5e4b59612154f2f2808dbe46ef6 /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.6.14@sha256:b8f8c20fc3308f8b1d00ccca2bc968e4e208af1c5c1069e1ad9753baa099acff /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./mise.toml ./mise.toml
|
||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202606161235@sha256:c6d59e3923f548d29a212b4dc51b6281a722cfa1da7972a009c0f3830f5762d6
|
||||
FROM ghcr.io/immich-app/base-server-prod:202607061334@sha256:c07ef6510ff2ccb6ead28434ade7c7a0c08aef33d7d3466b14beccdd431e86f7
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.6.10@sha256:f57ac375a262f52f8ac3f9101348dbff2187d5e4b59612154f2f2808dbe46ef6 /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.6.14@sha256:b8f8c20fc3308f8b1d00ccca2bc968e4e208af1c5c1069e1ad9753baa099acff /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \
|
||||
echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \
|
||||
|
||||
@@ -107,6 +107,6 @@
|
||||
<Input bind:value={() => getValue<string>(), setValue} />
|
||||
</Field>
|
||||
{:else}
|
||||
<Text>Unknown schema</Text>
|
||||
<Text>{$t('unknown_schema')}</Text>
|
||||
<CodeBlock code={JSON.stringify(schema, null, 2)} />
|
||||
{/if}
|
||||
|
||||
@@ -58,5 +58,14 @@ describe('i18n', () => {
|
||||
expect(getClosestAvailableLocale(['sr_Cyrl'], allLocales)).toBe('sr_Cyrl');
|
||||
expect(getClosestAvailableLocale(['zh_Hant'], allLocales)).toBe('zh_Hant');
|
||||
});
|
||||
|
||||
it('should handle language aliases', () => {
|
||||
const allLocales = ['zh-Hans', 'zh-Hant'];
|
||||
expect(getClosestAvailableLocale(['zh-CN'], allLocales)).toBe('zh-Hans');
|
||||
expect(getClosestAvailableLocale(['zh-HK'], allLocales)).toBe('zh-Hant');
|
||||
expect(getClosestAvailableLocale(['zh-MO'], allLocales)).toBe('zh-Hant');
|
||||
expect(getClosestAvailableLocale(['zh-SG'], allLocales)).toBe('zh-Hans');
|
||||
expect(getClosestAvailableLocale(['zh-TW'], allLocales)).toBe('zh-Hant');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
options={[
|
||||
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
|
||||
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
|
||||
{ label: $t('owner'), value: AlbumUserRole.Owner },
|
||||
{ label: $t('owner'), value: AlbumUserRole.Owner, disabled: true },
|
||||
{ label: $t('remove_user'), value: 'none' },
|
||||
] as SelectOption<AlbumUserRole | 'none'>[]}
|
||||
onChange={(value) => handleRoleSelect(user, value)}
|
||||
|
||||
@@ -13,6 +13,14 @@ export const getFormatter = async () => {
|
||||
return get(t);
|
||||
};
|
||||
|
||||
const aliases: Record<string, string> = {
|
||||
'zh-CN': 'zh-Hans',
|
||||
'zh-HK': 'zh-Hant',
|
||||
'zh-MO': 'zh-Hant',
|
||||
'zh-SG': 'zh-Hans',
|
||||
'zh-TW': 'zh-Hant',
|
||||
};
|
||||
|
||||
const modules = import.meta.glob('$i18n/*.json');
|
||||
|
||||
const fileCodes = Object.keys(modules)
|
||||
@@ -33,7 +41,9 @@ const getSubLocales = (locale: string) => {
|
||||
|
||||
export const getClosestAvailableLocale = (locales: readonly string[], allLocales: readonly string[]) => {
|
||||
const allLocalesSet = new Set(allLocales.map((locale) => convertBCP47(locale)));
|
||||
return locales.find((locale) => getSubLocales(locale).some((subLocale) => allLocalesSet.has(subLocale)));
|
||||
return locales
|
||||
.map((locale) => aliases[locale] ?? locale)
|
||||
.find((locale) => getSubLocales(locale).some((subLocale) => allLocalesSet.has(subLocale)));
|
||||
};
|
||||
|
||||
export const getPreferredLocale = () => getClosestAvailableLocale(navigator.languages, langCodes);
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@
|
||||
import { getFileSize } from '$lib/utils/asset-utils';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
@@ -25,7 +26,9 @@
|
||||
<Thumbnail asset={toTimelineAsset(asset)} readonly onClick={() => onViewAsset(asset)} thumbnailSize={boxWidth} />
|
||||
|
||||
{#if !!asset.libraryId}
|
||||
<div class="absolute inset-e-3 bottom-1 rounded-xl bg-red-500 px-4 py-1 text-xs transition-colors">External</div>
|
||||
<div class="absolute inset-e-3 bottom-1 rounded-xl bg-red-500 px-4 py-1 text-xs transition-colors">
|
||||
{$t('external')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-4 truncate px-4 text-center text-sm font-normal" title={asset.originalFileName}>
|
||||
|
||||
Reference in New Issue
Block a user