mirror of
https://github.com/immich-app/immich.git
synced 2026-07-22 21:34:19 +03:00
Compare commits
2 Commits
refactor/m
...
renovate/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479238c241 | ||
|
|
81f2c5fa9c |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -733,7 +733,7 @@ jobs:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Install server dependencies
|
||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
||||
run: pnpm --filter immich install --frozen-lockfile
|
||||
- name: Run API generation
|
||||
run: mise //:open-api
|
||||
working-directory: open-api
|
||||
@@ -792,7 +792,7 @@ jobs:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Install server dependencies
|
||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build plugins
|
||||
run: mise //:plugins
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "1.1.1"
|
||||
opentofu = "1.12.4"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
@@ -2,35 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.8"
|
||||
constraints = "4.52.8"
|
||||
version = "4.52.7"
|
||||
constraints = "4.52.7"
|
||||
hashes = [
|
||||
"h1:2uxVaMaGPDJljXqYzuD6xtyfOKdNudtGIgawRTkFJIs=",
|
||||
"h1:34HL1GDpqYdn2iEC+85pCi+NddZf30qH3fJ8y3P6CAM=",
|
||||
"h1:4Z+uD+YY/7HrqdypM7DyUK+RtWepoLBc9fqtRQYZx4I=",
|
||||
"h1:8Ss6wlJL2BHvkIZU1iaHvyUntqNhPFZ0fl6mjsxhctc=",
|
||||
"h1:AYeWWMTWaNg01N3hivxr/6YlG41j1RietMnft9vLjUI=",
|
||||
"h1:BGRNOzo8NUgbx5RwpWkWmr38f/s3txb7mzQhqfM5blI=",
|
||||
"h1:EEXRMdC8bPlpLuOU8u+dE8mFJe3BpquFdH6vXcKseY4=",
|
||||
"h1:ENp29e6O6OnaHJlbtmRlyjlAmm3Wt4mGctWUpBY3d0g=",
|
||||
"h1:IAtBAbGVe1npj/n1NvCIZkSYGl7qaPYT+FnVdZ2CZ00=",
|
||||
"h1:QRGGKngrxLORr4CImKXLDDJL5s67zUNBDAgk6FIIFNk=",
|
||||
"h1:bNql6cC+MfDbaJK3q7a8C1m3A21o7e5mrjBIGtk0RdI=",
|
||||
"h1:gmNv+PEOkYDdu+c15bfpcoAE3EI7pgmov9RMf3GZLsI=",
|
||||
"h1:yieZ7NWRYQcLYHUevzDDrrmDkoQICM+3ZeX4i/mw9ME=",
|
||||
"zh:08b305329a680a9213b2d8e642fbce7e4d97a524b1d2cef59e190ba9d678c477",
|
||||
"zh:47975bd711ee18a46e589822171fa87474a552b332bfc8dea8fd1a64504eed8d",
|
||||
"zh:5640d0d226bbafff3395542456c29feb942ca9c55ac01b4245a34c0590a33363",
|
||||
"zh:5b0ad839fafba938c60a95d6b4a865843643591e97573ed2b9c4af3754064f74",
|
||||
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
|
||||
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
|
||||
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
|
||||
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
|
||||
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
|
||||
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
|
||||
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
|
||||
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
|
||||
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
|
||||
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
|
||||
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
|
||||
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
|
||||
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
|
||||
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
|
||||
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
|
||||
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
|
||||
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
|
||||
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
|
||||
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:93a9bc1139f5c02a44fdbf51fb2ce0891e2a42033b66febb928ccf72e870408f",
|
||||
"zh:977f75cdf365686aa16ae02dcc0fc1769bae6f86be6e165393756c940bfb4af0",
|
||||
"zh:9afcda2660b3dc6ee6329acea532a719225bd1f6cf46f695feb2d77160847c1b",
|
||||
"zh:9e3da67b1b05b03d1f0c18b8677edf3797815b5dd49629f10eb2f457dccbed30",
|
||||
"zh:b8d7da230f5266367c1b6b1cf31aa39087e231a87d55b71bb9d5c854ca1fcfe6",
|
||||
"zh:cc96e7cd7350456b7a11a53d1c76c73a542de8359f9750f637ff865e8e77be91",
|
||||
"zh:da1f58d067def243047bb7178cb197e2b9c3a791a9eb380ad92b73290615ae29",
|
||||
"zh:ed5f3e1f59a338bcdce0a2537a8a2f299cfe18c168626799b2a9c97af3d8d3cb",
|
||||
"zh:f16cc31f73a58a26ffe2d93223eddb7c340623a4e2ef38c6091766fd611e3e11",
|
||||
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
|
||||
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
|
||||
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
|
||||
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
|
||||
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
|
||||
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
|
||||
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
|
||||
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
|
||||
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.8"
|
||||
version = "4.52.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,35 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.8"
|
||||
constraints = "4.52.8"
|
||||
version = "4.52.7"
|
||||
constraints = "4.52.7"
|
||||
hashes = [
|
||||
"h1:2uxVaMaGPDJljXqYzuD6xtyfOKdNudtGIgawRTkFJIs=",
|
||||
"h1:34HL1GDpqYdn2iEC+85pCi+NddZf30qH3fJ8y3P6CAM=",
|
||||
"h1:4Z+uD+YY/7HrqdypM7DyUK+RtWepoLBc9fqtRQYZx4I=",
|
||||
"h1:8Ss6wlJL2BHvkIZU1iaHvyUntqNhPFZ0fl6mjsxhctc=",
|
||||
"h1:AYeWWMTWaNg01N3hivxr/6YlG41j1RietMnft9vLjUI=",
|
||||
"h1:BGRNOzo8NUgbx5RwpWkWmr38f/s3txb7mzQhqfM5blI=",
|
||||
"h1:EEXRMdC8bPlpLuOU8u+dE8mFJe3BpquFdH6vXcKseY4=",
|
||||
"h1:ENp29e6O6OnaHJlbtmRlyjlAmm3Wt4mGctWUpBY3d0g=",
|
||||
"h1:IAtBAbGVe1npj/n1NvCIZkSYGl7qaPYT+FnVdZ2CZ00=",
|
||||
"h1:QRGGKngrxLORr4CImKXLDDJL5s67zUNBDAgk6FIIFNk=",
|
||||
"h1:bNql6cC+MfDbaJK3q7a8C1m3A21o7e5mrjBIGtk0RdI=",
|
||||
"h1:gmNv+PEOkYDdu+c15bfpcoAE3EI7pgmov9RMf3GZLsI=",
|
||||
"h1:yieZ7NWRYQcLYHUevzDDrrmDkoQICM+3ZeX4i/mw9ME=",
|
||||
"zh:08b305329a680a9213b2d8e642fbce7e4d97a524b1d2cef59e190ba9d678c477",
|
||||
"zh:47975bd711ee18a46e589822171fa87474a552b332bfc8dea8fd1a64504eed8d",
|
||||
"zh:5640d0d226bbafff3395542456c29feb942ca9c55ac01b4245a34c0590a33363",
|
||||
"zh:5b0ad839fafba938c60a95d6b4a865843643591e97573ed2b9c4af3754064f74",
|
||||
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
|
||||
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
|
||||
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
|
||||
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
|
||||
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
|
||||
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
|
||||
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
|
||||
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
|
||||
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
|
||||
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
|
||||
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
|
||||
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
|
||||
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
|
||||
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
|
||||
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
|
||||
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
|
||||
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
|
||||
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
|
||||
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:93a9bc1139f5c02a44fdbf51fb2ce0891e2a42033b66febb928ccf72e870408f",
|
||||
"zh:977f75cdf365686aa16ae02dcc0fc1769bae6f86be6e165393756c940bfb4af0",
|
||||
"zh:9afcda2660b3dc6ee6329acea532a719225bd1f6cf46f695feb2d77160847c1b",
|
||||
"zh:9e3da67b1b05b03d1f0c18b8677edf3797815b5dd49629f10eb2f457dccbed30",
|
||||
"zh:b8d7da230f5266367c1b6b1cf31aa39087e231a87d55b71bb9d5c854ca1fcfe6",
|
||||
"zh:cc96e7cd7350456b7a11a53d1c76c73a542de8359f9750f637ff865e8e77be91",
|
||||
"zh:da1f58d067def243047bb7178cb197e2b9c3a791a9eb380ad92b73290615ae29",
|
||||
"zh:ed5f3e1f59a338bcdce0a2537a8a2f299cfe18c168626799b2a9c97af3d8d3cb",
|
||||
"zh:f16cc31f73a58a26ffe2d93223eddb7c340623a4e2ef38c6091766fd611e3e11",
|
||||
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
|
||||
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
|
||||
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
|
||||
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
|
||||
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
|
||||
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
|
||||
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
|
||||
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
|
||||
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.8"
|
||||
version = "4.52.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.4-ubuntu@sha256:df2e7ef5f32f771794cf76bad5f2bceac227036460a2cc269a9045e5662abc58
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
BIN
docs/docs/administration/img/reset-admin-password.webp
Normal file
BIN
docs/docs/administration/img/reset-admin-password.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@@ -35,7 +35,6 @@ Found Admin:
|
||||
- Email=admin@example.com
|
||||
- Name=Immich Admin
|
||||
? Please choose a new password (optional) immich-is-cool
|
||||
? Invalidate existing sessions? Yes
|
||||
The admin password has been updated.
|
||||
```
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sharp": "^0.35.2",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
|
||||
@@ -650,7 +650,7 @@ describe(`immich upload`, () => {
|
||||
]);
|
||||
|
||||
expect(stdout).toBe('');
|
||||
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '--skip-hash'`);
|
||||
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
|
||||
expect(exitCode).not.toBe(0);
|
||||
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
|
||||
@@ -65,10 +65,6 @@ describe(`immich-admin`, () => {
|
||||
child.stdout.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
if (data.includes('Please choose a new password (optional)')) {
|
||||
child.stdin.write('\n');
|
||||
}
|
||||
|
||||
if (data.includes('Invalidate existing sessions?')) {
|
||||
child.stdin.end('\n');
|
||||
}
|
||||
});
|
||||
|
||||
132
mise.lock
132
mise.lock
@@ -94,23 +94,6 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||
|
||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
|
||||
|
||||
@@ -129,12 +112,17 @@ version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
|
||||
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||
|
||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||
version = "7.1.3-6"
|
||||
@@ -145,6 +133,18 @@ checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
|
||||
|
||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
|
||||
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
|
||||
|
||||
[[tools."github:webassembly/binaryen"]]
|
||||
version = "version_124"
|
||||
backend = "github:webassembly/binaryen"
|
||||
@@ -248,43 +248,43 @@ version = "7.5.0"
|
||||
backend = "npm:oazapfts"
|
||||
|
||||
[[tools.opentofu]]
|
||||
version = "1.12.4"
|
||||
version = "1.11.6"
|
||||
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: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: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: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: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: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: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: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: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: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: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: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: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: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.13.1"
|
||||
@@ -327,40 +327,40 @@ url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563981"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
version = "1.1.1"
|
||||
version = "1.0.3"
|
||||
backend = "aqua:gruntwork-io/terragrunt"
|
||||
|
||||
[tools.terragrunt."platforms.linux-arm64"]
|
||||
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
|
||||
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:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
|
||||
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:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
|
||||
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:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
|
||||
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:9ec8f678b9ae6c81d5d9d77b94cbf6349ce639d5938694b4adc9dea73e416794"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785628"
|
||||
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:73e768a69fa44a60f9d9174f54aaf179327e08706f633c9c79d5f5c9622c91c2"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785520"
|
||||
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:dd50a324691e072a3ac879e9ec43d6310e2936c929fda658eb2811fefcc75115"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_windows_amd64.exe.tar.gz"
|
||||
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786421"
|
||||
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,4 +1,4 @@
|
||||
monorepo_root = true
|
||||
experimental_monorepo_root = true
|
||||
|
||||
[monorepo]
|
||||
config_roots = [
|
||||
@@ -17,8 +17,8 @@ config_roots = [
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
pnpm = "11.13.1"
|
||||
terragrunt = "1.1.1"
|
||||
opentofu = "1.12.4"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
"github:extism/cli" = "1.6.3"
|
||||
"github:webassembly/binaryen" = "version_124"
|
||||
@@ -35,6 +35,7 @@ macos-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" }
|
||||
macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" }
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
pin = true
|
||||
lockfile = true
|
||||
|
||||
@@ -64,7 +65,6 @@ dir = "open-api"
|
||||
run = "bash ./bin/generate-dart-sdk.sh"
|
||||
|
||||
[tasks.open-api]
|
||||
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
|
||||
run = [
|
||||
{ task = "//:plugins" },
|
||||
{ task = "//server:install" },
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
@@ -33,7 +33,7 @@ class PartnerPage extends ConsumerWidget {
|
||||
title: Text(context.t.partners),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
actions: const [PartnerAddAction(display: ActionDisplay.iconButton)],
|
||||
actions: const [ActionIconButtonWidget(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: PartnerAddAction(),
|
||||
child: ActionButtonWidget(action: PartnerAddAction()),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -88,10 +88,8 @@ class PartnerSharedByList extends StatelessWidget {
|
||||
leading: PartnerUserAvatar(userId: partner.id, name: partner.name),
|
||||
title: Text(partner.name),
|
||||
subtitle: Text(partner.email),
|
||||
trailing: PartnerRemoveAction(
|
||||
sharedWithId: partner.id,
|
||||
partnerName: partner.name,
|
||||
display: ActionDisplay.iconButton,
|
||||
trailing: ActionIconButtonWidget(
|
||||
action: PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import 'package:flutter/widgets.dart' hide Action;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
enum ActionDisplay { button, iconButton, menuItem, columnButton }
|
||||
class ActionScope {
|
||||
final BuildContext context;
|
||||
final WidgetRef ref;
|
||||
final UserDto authUser;
|
||||
|
||||
class Action {
|
||||
final IconData icon;
|
||||
final bool isVisible;
|
||||
final String label;
|
||||
final Future<void> Function() onAction;
|
||||
|
||||
const Action({
|
||||
required this.icon,
|
||||
this.isVisible = true,
|
||||
required this.label,
|
||||
required this.onAction,
|
||||
});
|
||||
const ActionScope({required this.context, required this.ref, required this.authUser});
|
||||
}
|
||||
|
||||
abstract class BaseAction {
|
||||
const BaseAction();
|
||||
|
||||
IconData get icon;
|
||||
|
||||
String label(ActionScope scope);
|
||||
|
||||
bool isVisible(ActionScope scope) => true;
|
||||
|
||||
Future<void> onAction(ActionScope scope);
|
||||
}
|
||||
|
||||
abstract class AssetAction<T extends BaseAsset> extends BaseAction {
|
||||
final Iterable<BaseAsset> assets;
|
||||
|
||||
const AssetAction({required this.assets});
|
||||
|
||||
Iterable<T> filter(ActionScope scope) => assets.whereType<T>();
|
||||
}
|
||||
|
||||
@@ -1,30 +1,95 @@
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
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';
|
||||
|
||||
abstract class ActionWidget extends ConsumerWidget {
|
||||
final ActionDisplay display;
|
||||
class _ActionWidgetScope {
|
||||
final String label;
|
||||
final VoidCallback onAction;
|
||||
|
||||
const ActionWidget({super.key, this.display = ActionDisplay.button});
|
||||
const _ActionWidgetScope({required this.label, required this.onAction});
|
||||
}
|
||||
|
||||
Action resolve(BuildContext context, WidgetRef ref);
|
||||
class _ActionWidget extends ConsumerWidget {
|
||||
final BaseAction action;
|
||||
final Widget Function(_ActionWidgetScope context) builder;
|
||||
|
||||
const _ActionWidget({required this.action, required this.builder});
|
||||
|
||||
Future<void> _onAction(ActionScope scope) async {
|
||||
try {
|
||||
await action.onAction(scope);
|
||||
} catch (error, stackTrace) {
|
||||
handleError(scope.context, error, stack: stackTrace, description: 'Action failed: ${action.runtimeType}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final action = resolve(context, ref);
|
||||
if (!action.isVisible) {
|
||||
final authUser = ref.watch(currentUserProvider);
|
||||
if (authUser == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return switch (display) {
|
||||
ActionDisplay.button =>
|
||||
ImmichTextButton(labelText: action.label, icon: action.icon, onPressed: action.onAction),
|
||||
ActionDisplay.iconButton => ImmichIconButton(icon: action.icon, onPressed: action.onAction),
|
||||
ActionDisplay.menuItem =>
|
||||
ImmichMenuItem(icon: action.icon, label: action.label, onPressed: action.onAction),
|
||||
ActionDisplay.columnButton =>
|
||||
ImmichColumnButton(icon: action.icon, label: action.label, onPressed: action.onAction),
|
||||
};
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
class ActionIconButtonWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
final ImmichVariant variant;
|
||||
|
||||
const ActionIconButtonWidget({super.key, required this.action, this.variant = .ghost});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ActionWidget(
|
||||
action: action,
|
||||
builder: (ctx) => ImmichIconButton(icon: action.icon, onPressed: ctx.onAction, variant: variant),
|
||||
);
|
||||
}
|
||||
|
||||
class ActionButtonWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
final ImmichVariant variant;
|
||||
|
||||
const ActionButtonWidget({super.key, required this.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),
|
||||
);
|
||||
}
|
||||
|
||||
class ActionColumnButtonWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
|
||||
const ActionColumnButtonWidget({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ActionWidget(
|
||||
action: action,
|
||||
builder: (ctx) => ImmichColumnButton(icon: action.icon, label: ctx.label, onPressed: ctx.onAction),
|
||||
);
|
||||
}
|
||||
|
||||
class ActionMenuItemWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
|
||||
const ActionMenuItemWidget({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ActionWidget(
|
||||
action: action,
|
||||
builder: (ctx) => ImmichMenuItem(icon: action.icon, label: ctx.label, onPressed: ctx.onAction),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
typedef AssetActionSelection = ({Set<BaseAsset> assets, void Function() clearSelect});
|
||||
|
||||
final assetsActionProvider = Provider.family<AssetActionSelection, ActionSource>((ref, source) {
|
||||
final assets = switch (source) {
|
||||
ActionSource.timeline => ref.watch(multiSelectProvider.select((s) => s.selectedAssets)),
|
||||
ActionSource.viewer => switch (ref.watch(assetViewerProvider.select((s) => s.currentAsset))) {
|
||||
BaseAsset asset => {asset},
|
||||
null => const <BaseAsset>{},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
assets: assets,
|
||||
clearSelect: () {
|
||||
if (source == ActionSource.timeline) {
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
abstract class AssetAction extends ActionWidget {
|
||||
final ActionSource source;
|
||||
|
||||
const AssetAction({super.key, super.display, this.source = ActionSource.timeline});
|
||||
}
|
||||
@@ -1,30 +1,27 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class AssetDebugAction extends AssetAction {
|
||||
const AssetDebugAction({super.key, super.display, super.source});
|
||||
class AssetDebugAction extends AssetAction<BaseAsset> {
|
||||
const AssetDebugAction({required super.assets});
|
||||
|
||||
@override
|
||||
Action resolve(BuildContext context, WidgetRef ref) {
|
||||
final (:assets, :clearSelect) = ref.watch(assetsActionProvider(source));
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting);
|
||||
IconData get icon => Icons.help_outline_rounded;
|
||||
|
||||
return Action(
|
||||
icon: Icons.help_outline_rounded,
|
||||
isVisible: assets.length == 1 && advancedTroubleshooting,
|
||||
label: context.t.troubleshoot,
|
||||
onAction: () async {
|
||||
unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
|
||||
clearSelect();
|
||||
},
|
||||
);
|
||||
}
|
||||
@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<void> onAction(ActionScope scope) async =>
|
||||
unawaited(scope.context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
|
||||
}
|
||||
|
||||
@@ -1,60 +1,38 @@
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class FavoriteAction extends AssetAction {
|
||||
const FavoriteAction({super.key, super.display, super.source});
|
||||
class FavoriteAction extends AssetAction<RemoteAsset> {
|
||||
final bool favorite;
|
||||
|
||||
FavoriteAction({required super.assets}) : favorite = assets.any((asset) => !asset.isFavorite);
|
||||
|
||||
@override
|
||||
Action resolve(BuildContext context, WidgetRef ref) {
|
||||
final (:assets, :clearSelect) = ref.watch(assetsActionProvider(source));
|
||||
final userId = ref.read(currentUserProvider)?.id;
|
||||
final targets = userId == null
|
||||
? const <RemoteAsset>[]
|
||||
: AssetFilter(assets).owned(userId).favorite(isFavorite: false).toList(growable: false);
|
||||
|
||||
return Action(
|
||||
icon: Icons.favorite_border_rounded,
|
||||
isVisible: targets.isNotEmpty,
|
||||
label: context.t.favorite,
|
||||
onAction: () async {
|
||||
final ids = targets.map((asset) => asset.id).toList(growable: false);
|
||||
await ref.read(assetServiceProvider).updateFavorite(ids, true);
|
||||
snackbar.success(StaticTranslations.instance.favorite_action_prompt(count: ids.length));
|
||||
clearSelect();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnfavoriteAction extends AssetAction {
|
||||
const UnfavoriteAction({super.key, super.display, super.source});
|
||||
IconData get icon => favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
|
||||
|
||||
@override
|
||||
Action resolve(BuildContext context, WidgetRef ref) {
|
||||
final (:assets, :clearSelect) = ref.watch(assetsActionProvider(source));
|
||||
final userId = ref.read(currentUserProvider)?.id;
|
||||
final targets = userId == null
|
||||
? const <RemoteAsset>[]
|
||||
: AssetFilter(assets).owned(userId).favorite(isFavorite: true).toList(growable: false);
|
||||
String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite;
|
||||
|
||||
return Action(
|
||||
icon: Icons.favorite_rounded,
|
||||
isVisible: targets.isNotEmpty,
|
||||
label: context.t.unfavorite,
|
||||
onAction: () async {
|
||||
final ids = targets.map((asset) => asset.id).toList(growable: false);
|
||||
await ref.read(assetServiceProvider).updateFavorite(ids, false);
|
||||
snackbar.success(StaticTranslations.instance.unfavorite_action_prompt(count: ids.length));
|
||||
clearSelect();
|
||||
},
|
||||
);
|
||||
@override
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) =>
|
||||
AssetFilter(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite);
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
|
||||
|
||||
@override
|
||||
Future<void> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,69 @@
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.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/widgets/common/confirm_dialog.dart';
|
||||
|
||||
class PartnerAddAction extends ActionWidget {
|
||||
const PartnerAddAction({super.key, super.display});
|
||||
class PartnerAddAction extends BaseAction {
|
||||
const PartnerAddAction();
|
||||
|
||||
@override
|
||||
Action resolve(BuildContext context, WidgetRef ref) {
|
||||
return Action(
|
||||
icon: Icons.person_add_rounded,
|
||||
label: context.t.add_partner,
|
||||
onAction: () async {
|
||||
final authUser = ref.read(currentUserProvider);
|
||||
if (authUser == null) {
|
||||
return;
|
||||
}
|
||||
IconData get icon => Icons.person_add_rounded;
|
||||
|
||||
final selected = await showDialog<User>(context: context, builder: (_) => const PartnerSelectionDialog());
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
@override
|
||||
String label(ActionScope scope) => scope.context.t.add_partner;
|
||||
|
||||
await ref.read(partnerServiceProvider).create(sharedById: authUser.id, sharedWithId: selected.id);
|
||||
},
|
||||
);
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async {
|
||||
final ActionScope(:context, :ref, :authUser) = scope;
|
||||
final selected = await showDialog<User>(context: context, builder: (_) => const PartnerSelectionDialog());
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(partnerServiceProvider).create(sharedById: authUser.id, sharedWithId: selected.id);
|
||||
}
|
||||
}
|
||||
|
||||
class PartnerRemoveAction extends ActionWidget {
|
||||
class PartnerRemoveAction extends BaseAction {
|
||||
const PartnerRemoveAction({required this.sharedWithId, required this.partnerName});
|
||||
|
||||
final String sharedWithId;
|
||||
final String partnerName;
|
||||
|
||||
const PartnerRemoveAction({super.key, super.display, required this.sharedWithId, required this.partnerName});
|
||||
@override
|
||||
IconData get icon => Icons.person_remove_rounded;
|
||||
|
||||
@override
|
||||
Action resolve(BuildContext context, WidgetRef ref) {
|
||||
return Action(
|
||||
icon: Icons.person_remove_rounded,
|
||||
label: context.t.remove,
|
||||
onAction: () async {
|
||||
final authUser = ref.read(currentUserProvider);
|
||||
if (authUser == null) {
|
||||
return;
|
||||
}
|
||||
String label(ActionScope scope) => scope.context.t.remove;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => ConfirmDialog(
|
||||
title: context.t.stop_photo_sharing,
|
||||
content: context.t.partner_page_stop_sharing_content(partner: partnerName),
|
||||
),
|
||||
);
|
||||
if (confirmed != true) {
|
||||
return;
|
||||
}
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async {
|
||||
final ActionScope(:context, :ref, :authUser) = scope;
|
||||
|
||||
await ref.read(partnerServiceProvider).delete(sharedById: authUser.id, sharedWithId: sharedWithId);
|
||||
},
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => ConfirmDialog(
|
||||
title: context.t.stop_photo_sharing,
|
||||
content: context.t.partner_page_stop_sharing_content(partner: partnerName),
|
||||
),
|
||||
);
|
||||
if (confirmed != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(partnerServiceProvider).delete(sharedById: authUser.id, sharedWithId: sharedWithId);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
final candidatesStateProvider = StreamProvider.autoDispose<Iterable<User>>((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();
|
||||
}
|
||||
|
||||
24
mobile/lib/presentation/actions/timeline.action.dart
Normal file
24
mobile/lib/presentation/actions/timeline.action.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
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<void> onAction(ActionScope scope) async {
|
||||
await action.onAction(scope);
|
||||
scope.ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
||||
@@ -44,6 +43,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
final assetForAction = [asset];
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
@@ -61,8 +61,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
},
|
||||
),
|
||||
|
||||
const FavoriteAction(source: ActionSource.viewer, display: ActionDisplay.iconButton),
|
||||
const UnfavoriteAction(source: ActionSource.viewer, display: ActionDisplay.iconButton),
|
||||
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
|
||||
|
||||
ImmichColorOverride(color: null, child: ViewerKebabMenu(originalTheme: originalTheme)),
|
||||
];
|
||||
|
||||
@@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.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';
|
||||
@@ -75,6 +76,9 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
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,
|
||||
@@ -85,7 +89,7 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteAction(source: ActionSource.timeline, display: ActionDisplay.columnButton),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
|
||||
@@ -4,8 +4,9 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.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';
|
||||
@@ -73,6 +74,9 @@ 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,
|
||||
@@ -81,7 +85,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const FavoriteAction(source: ActionSource.timeline, display: ActionDisplay.columnButton),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
|
||||
@@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.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/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';
|
||||
@@ -82,6 +83,9 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
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,
|
||||
@@ -89,7 +93,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const AssetDebugAction(source: ActionSource.timeline, display: ActionDisplay.columnButton),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -3,8 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.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';
|
||||
@@ -84,6 +85,9 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
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,
|
||||
@@ -97,7 +101,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
|
||||
if (ownsAlbum) ...[
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteAction(source: ActionSource.timeline, display: ActionDisplay.columnButton),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
],
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
if (ownsAlbum) ...[
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
@visibleForTesting
|
||||
bool isRouteInStack(Ref ref, String routeName) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
void onChange() => ref.invalidateSelf();
|
||||
router.addListener(onChange);
|
||||
ref.onDispose(() => router.removeListener(onChange));
|
||||
return router.stackData.any((route) => route.name == routeName);
|
||||
}
|
||||
|
||||
final inLockedViewProvider = Provider<bool>((ref) => isRouteInStack(ref, DriftLockedFolderRoute.name));
|
||||
final isAssetViewerOpenProvider = Provider<bool>((ref) => isRouteInStack(ref, AssetViewerRoute.name));
|
||||
|
||||
final inLockedViewProvider = StateProvider<bool>((ref) => false);
|
||||
final isAssetViewerOpenProvider = StateProvider<bool>((ref) => false);
|
||||
final currentRouteNameProvider = StateProvider<String?>((ref) => null);
|
||||
final previousRouteNameProvider = StateProvider<String?>((ref) => null);
|
||||
final previousRouteDataProvider = StateProvider<RouteSettings?>((ref) => null);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class AppNavigationObserver extends AutoRouterObserver {
|
||||
/// Riverpod Instance
|
||||
@@ -11,12 +12,46 @@ class AppNavigationObserver extends AutoRouterObserver {
|
||||
|
||||
AppNavigationObserver({required this.ref});
|
||||
|
||||
@override
|
||||
Future<void> didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async {
|
||||
unawaited(Future(() => ref.read(inLockedViewProvider.notifier).state = false));
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush(Route route, Route? previousRoute) {
|
||||
_handleDriftLockedFolderState(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;
|
||||
if (route.settings.name == AssetViewerRoute.name) {
|
||||
ref.read(isAssetViewerOpenProvider.notifier).state = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop(Route route, Route? previousRoute) {
|
||||
_handleDriftLockedFolderState(previousRoute ?? route, null);
|
||||
if (route.settings.name == AssetViewerRoute.name) {
|
||||
Future(() => ref.read(isAssetViewerOpenProvider.notifier).state = false);
|
||||
}
|
||||
}
|
||||
|
||||
_handleDriftLockedFolderState(Route route, Route? previousRoute) {
|
||||
final isInLockedView = ref.read(inLockedViewProvider);
|
||||
final isFromLockedViewToDetailView =
|
||||
route.settings.name == AssetViewerRoute.name && previousRoute?.settings.name == DriftLockedFolderRoute.name;
|
||||
|
||||
final isFromDetailViewToInfoPanelView =
|
||||
route.settings.name == null && previousRoute?.settings.name == AssetViewerRoute.name && isInLockedView;
|
||||
|
||||
if (route.settings.name == DriftLockedFolderRoute.name ||
|
||||
isFromLockedViewToDetailView ||
|
||||
isFromDetailViewToInfoPanelView) {
|
||||
Future(() => ref.read(inLockedViewProvider.notifier).state = true);
|
||||
} else {
|
||||
Future(() => ref.read(inLockedViewProvider.notifier).state = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,10 +290,13 @@ class BackgroundUploadService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||
// Some apps (e.g. DJI/Fusion) return names without an extension; fall back to the asset name for those.
|
||||
final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name);
|
||||
final originalFileName = p.setExtension(fileName, extension);
|
||||
String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||
final hasExtension = p.extension(fileName).isNotEmpty;
|
||||
if (!hasExtension) {
|
||||
fileName = p.setExtension(fileName, p.extension(asset.name));
|
||||
}
|
||||
|
||||
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
|
||||
|
||||
String metadata = UploadTaskMetadata(
|
||||
localAssetId: asset.id,
|
||||
|
||||
@@ -309,10 +309,17 @@ class ForegroundUploadService {
|
||||
return;
|
||||
}
|
||||
|
||||
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||
// Some apps (e.g. DJI/Fusion) return names without an extension; fall back to the asset name for those.
|
||||
final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name);
|
||||
final originalFileName = p.setExtension(fileName, extension);
|
||||
String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||
|
||||
/// Handle special file name from DJI or Fusion app
|
||||
/// If the file name has no extension, likely due to special renaming template by specific apps
|
||||
/// we append the original extension from the asset name
|
||||
final hasExtension = p.extension(fileName).isNotEmpty;
|
||||
if (!hasExtension) {
|
||||
fileName = p.setExtension(fileName, p.extension(asset.name));
|
||||
}
|
||||
|
||||
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
|
||||
final deviceId = Store.get(StoreKey.deviceId);
|
||||
|
||||
final fields = {
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.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/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
@@ -192,7 +192,7 @@ enum ActionButtonType {
|
||||
bool menuItem = false,
|
||||
]) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => AssetDebugAction(source: context.source, display: ActionDisplay.menuItem),
|
||||
ActionButtonType.advancedInfo => ActionMenuItemWidget(action: AssetDebugAction(assets: [context.asset])),
|
||||
ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.shareLink => ShareLinkActionButton(
|
||||
source: context.source,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
@@ -9,7 +10,6 @@ import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
@@ -255,11 +255,11 @@ class _BackupDelaySlider extends ConsumerWidget {
|
||||
_ => 600,
|
||||
};
|
||||
|
||||
static String formatBackupDelaySliderValue(BuildContext context, int v) => switch (v) {
|
||||
0 => context.t.setting_notifications_notify_seconds(count: 5),
|
||||
1 => context.t.setting_notifications_notify_seconds(count: 30),
|
||||
2 => context.t.setting_notifications_notify_minutes(count: 2),
|
||||
_ => context.t.setting_notifications_notify_minutes(count: 10),
|
||||
static String formatBackupDelaySliderValue(int v) => switch (v) {
|
||||
0 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '5'}),
|
||||
1 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '30'}),
|
||||
2 => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '2'}),
|
||||
_ => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '10'}),
|
||||
};
|
||||
|
||||
@override
|
||||
@@ -272,8 +272,8 @@ class _BackupDelaySlider extends ConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24.0, top: 8.0),
|
||||
child: Text(
|
||||
context.t.backup_controller_page_background_delay(
|
||||
duration: formatBackupDelaySliderValue(context, currentValue),
|
||||
'backup_controller_page_background_delay'.tr(
|
||||
namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)},
|
||||
),
|
||||
style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
@@ -291,7 +291,7 @@ class _BackupDelaySlider extends ConsumerWidget {
|
||||
max: 3.0,
|
||||
min: 0.0,
|
||||
divisions: 3,
|
||||
label: formatBackupDelaySliderValue(context, currentValue),
|
||||
label: formatBackupDelaySliderValue(currentValue),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -136,51 +136,6 @@ void main() {
|
||||
expect(task, isNotNull);
|
||||
expect(task!.fields.containsKey('visibility'), isFalse);
|
||||
});
|
||||
|
||||
test('corrects the extension when iOS returns a rendered file for a .dng asset', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final mockFile = File('/path/to/IMG_6499.jpg');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_6499.dng');
|
||||
|
||||
final task = await sut.getUploadTask(asset);
|
||||
expect(task, isNotNull);
|
||||
expect(task!.fields['filename'], equals('IMG_6499.jpg'));
|
||||
});
|
||||
|
||||
test('keeps the .dng extension for a genuine RAW original', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final mockFile = File('/path/to/IMG_5210.dng');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_5210.dng');
|
||||
|
||||
final task = await sut.getUploadTask(asset);
|
||||
expect(task, isNotNull);
|
||||
expect(task!.fields['filename'], equals('IMG_5210.dng'));
|
||||
});
|
||||
|
||||
test('borrows the extension from the asset name for an extensionless name (DJI/Fusion)', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final mockFile = File('/path/to/DJI_0001');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'DJI_0001');
|
||||
|
||||
final task = await sut.getUploadTask(asset);
|
||||
expect(task, isNotNull);
|
||||
expect(task!.fields['filename'], equals('DJI_0001.jpg'));
|
||||
});
|
||||
});
|
||||
|
||||
group('getLivePhotoUploadTask', () {
|
||||
|
||||
@@ -81,24 +81,6 @@ void main() {
|
||||
return captured;
|
||||
}
|
||||
|
||||
List<String> captureOriginalFileNames() {
|
||||
final captured = <String>[];
|
||||
when(
|
||||
() => mockUploadRepository.uploadFile(
|
||||
file: any(named: 'file'),
|
||||
originalFileName: any(named: 'originalFileName'),
|
||||
fields: any(named: 'fields'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
onProgress: any(named: 'onProgress'),
|
||||
logContext: any(named: 'logContext'),
|
||||
),
|
||||
).thenAnswer((invocation) async {
|
||||
captured.add(invocation.namedArguments[#originalFileName] as String);
|
||||
return UploadResult.success(remoteAssetId: 'remote-${captured.length}');
|
||||
});
|
||||
return captured;
|
||||
}
|
||||
|
||||
group('uploadSingleAsset', () {
|
||||
test('should upload the motion part hidden and keep the still image visible', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
@@ -142,59 +124,5 @@ void main() {
|
||||
expect(captured, hasLength(1));
|
||||
expect(captured[0].containsKey('visibility'), isFalse);
|
||||
});
|
||||
|
||||
test('corrects the extension when iOS returns a rendered file for a .dng asset', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/IMG_6499.jpg');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_6499.dng');
|
||||
|
||||
final names = captureOriginalFileNames();
|
||||
|
||||
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
|
||||
|
||||
expect(names, equals(['IMG_6499.jpg']));
|
||||
});
|
||||
|
||||
test('keeps the .dng extension for a genuine RAW original', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/IMG_5210.dng');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'IMG_5210.dng');
|
||||
|
||||
final names = captureOriginalFileNames();
|
||||
|
||||
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
|
||||
|
||||
expect(names, equals(['IMG_5210.dng']));
|
||||
});
|
||||
|
||||
test('borrows the extension from the asset name for an extensionless name (DJI/Fusion)', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/DJI_0001');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'DJI_0001');
|
||||
|
||||
final names = captureOriginalFileNames();
|
||||
|
||||
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
|
||||
|
||||
expect(names, equals(['DJI_0001.jpg']));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
@@ -23,18 +20,11 @@ void main() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
List<Override> selection(Set<BaseAsset> assets) => [
|
||||
multiSelectProvider.overrideWith(
|
||||
() => MultiSelectNotifier(MultiSelectState(selectedAssets: assets, lockedSelectionAssets: const {})),
|
||||
),
|
||||
];
|
||||
|
||||
group('AssetDebugAction', () {
|
||||
testWidgets('visible for a single asset when advanced troubleshooting is on', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const AssetDebugAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({RemoteAssetFactory.create()}),
|
||||
ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])),
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsOneWidget);
|
||||
@@ -43,8 +33,9 @@ void main() {
|
||||
testWidgets('hidden for multiple assets', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const AssetDebugAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({RemoteAssetFactory.create(), RemoteAssetFactory.create()}),
|
||||
ActionIconButtonWidget(
|
||||
action: AssetDebugAction(assets: [RemoteAssetFactory.create(), RemoteAssetFactory.create()]),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
@@ -54,8 +45,7 @@ void main() {
|
||||
await StoreService.I.put(StoreKey.advancedTroubleshooting, false);
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const AssetDebugAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({RemoteAssetFactory.create()}),
|
||||
ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])),
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../domain/service.mock.dart';
|
||||
@@ -27,34 +24,28 @@ void main() {
|
||||
RemoteAsset owned({bool isFavorite = false}) =>
|
||||
RemoteAssetFactory.create(ownerId: context.currentUser.id, isFavorite: isFavorite);
|
||||
|
||||
List<Override> selection(Set<BaseAsset> assets) => [
|
||||
multiSelectProvider.overrideWith(
|
||||
() => MultiSelectNotifier(MultiSelectState(selectedAssets: assets, lockedSelectionAssets: const {})),
|
||||
),
|
||||
];
|
||||
|
||||
group('FavoriteAction', () {
|
||||
testWidgets('favorites the eligible owned assets', (tester) async {
|
||||
final asset = owned();
|
||||
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
const FavoriteAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({asset}),
|
||||
);
|
||||
await tester.pumpTestAction(context, FavoriteAction(assets: [asset]));
|
||||
|
||||
verify(() => assetService.updateFavorite([asset.id], true)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('unfavorite the eligible owned assets', (tester) async {
|
||||
final asset = owned(isFavorite: true);
|
||||
|
||||
await tester.pumpTestAction(context, FavoriteAction(assets: [asset]));
|
||||
|
||||
verify(() => assetService.updateFavorite([asset.id], false)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('ignores assets owned by someone else', (tester) async {
|
||||
final mine = owned();
|
||||
final theirs = RemoteAssetFactory.create();
|
||||
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
const FavoriteAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({mine, theirs}),
|
||||
);
|
||||
await tester.pumpTestAction(context, FavoriteAction(assets: [mine, theirs]));
|
||||
|
||||
verify(() => assetService.updateFavorite([mine.id], true)).called(1);
|
||||
});
|
||||
@@ -63,51 +54,25 @@ void main() {
|
||||
final first = owned();
|
||||
final second = owned();
|
||||
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
const FavoriteAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({first, second}),
|
||||
);
|
||||
await tester.pumpTestAction(context, FavoriteAction(assets: [first, second]));
|
||||
|
||||
verify(() => assetService.updateFavorite([first.id, second.id], true)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('skips owned assets already favorited', (tester) async {
|
||||
testWidgets('skips owned assets already in the target state', (tester) async {
|
||||
final stale = owned();
|
||||
final alreadyFavorite = owned(isFavorite: true);
|
||||
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
const FavoriteAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({stale, alreadyFavorite}),
|
||||
);
|
||||
await tester.pumpTestAction(context, FavoriteAction(assets: [stale, alreadyFavorite]));
|
||||
|
||||
verify(() => assetService.updateFavorite([stale.id], true)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('shows a confirmation snackbar on success', (tester) async {
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
const FavoriteAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({owned()}),
|
||||
);
|
||||
await tester.pumpTestAction(context, FavoriteAction(assets: [owned()]));
|
||||
await tester.pumpUntilFound(find.byType(SnackBar));
|
||||
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('UnfavoriteAction', () {
|
||||
testWidgets('unfavorites the eligible owned assets', (tester) async {
|
||||
final asset = owned(isFavorite: true);
|
||||
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
const UnfavoriteAction(display: ActionDisplay.iconButton),
|
||||
overrides: selection({asset}),
|
||||
);
|
||||
|
||||
verify(() => assetService.updateFavorite([asset.id], false)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
|
||||
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/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
@@ -32,7 +31,7 @@ void main() {
|
||||
testWidgets('creates a partner for the selected candidate', (tester) async {
|
||||
final candidate = UserFactory.create();
|
||||
|
||||
await tester.pumpTestAction(context, const PartnerAddAction(display: ActionDisplay.iconButton), overrides: overrides(candidates: [candidate]));
|
||||
await tester.pumpTestAction(context, const PartnerAddAction(), overrides: overrides(candidates: [candidate]));
|
||||
await tester.pumpUntilFound(find.text(candidate.name));
|
||||
await tester.tap(find.text(candidate.name));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -43,7 +42,7 @@ void main() {
|
||||
testWidgets('creates nothing when the selection dialog is dismissed', (tester) async {
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
const PartnerAddAction(display: ActionDisplay.iconButton),
|
||||
const PartnerAddAction(),
|
||||
overrides: overrides(candidates: [UserFactory.create()]),
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape); // dismiss without selecting
|
||||
@@ -58,7 +57,7 @@ void main() {
|
||||
final partner = UserFactory.create();
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name, display: ActionDisplay.iconButton),
|
||||
PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name),
|
||||
overrides: overrides(),
|
||||
);
|
||||
await tester.tap(find.byType(TextButton).last); // confirm
|
||||
@@ -71,7 +70,7 @@ void main() {
|
||||
final partner = UserFactory.create();
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name, display: ActionDisplay.iconButton),
|
||||
PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name),
|
||||
overrides: overrides(),
|
||||
);
|
||||
await tester.tap(find.byType(TextButton).first); // cancel
|
||||
|
||||
108
mobile/test/unit/presentation/actions/timeline_action_test.dart
Normal file
108
mobile/test/unit/presentation/actions/timeline_action_test.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
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<void> 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<Override> 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -17,10 +17,12 @@ void main() {
|
||||
|
||||
group('PartnerSharedByList', () {
|
||||
testWidgets('shows the empty-state add button when there are no partners', (tester) async {
|
||||
final action = const PartnerAddAction();
|
||||
|
||||
await tester.pumpTestWidget(context, const PartnerSharedByList(partners: []));
|
||||
|
||||
expect(find.byType(ListView), findsNothing);
|
||||
expect(find.widgetWithIcon(TextButton, Icons.person_add_rounded), findsOneWidget);
|
||||
expect(find.widgetWithIcon(TextButton, action.icon), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders a tile per partner with name and email', (tester) async {
|
||||
@@ -37,8 +39,9 @@ void main() {
|
||||
testWidgets('renders a remove action for each partner', (tester) async {
|
||||
final partner1 = PartnerFactory.create(inTimeline: true);
|
||||
final partner2 = PartnerFactory.create();
|
||||
final action = const PartnerRemoveAction(sharedWithId: '', partnerName: '');
|
||||
await tester.pumpTestWidget(context, PartnerSharedByList(partners: [partner1, partner2]));
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsNWidgets(2));
|
||||
expect(find.byIcon(action.icon), findsNWidgets(2));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
@@ -97,10 +98,10 @@ extension PumpPresentationWidget on WidgetTester {
|
||||
|
||||
Future<void> pumpTestAction(
|
||||
PresentationContext context,
|
||||
ActionWidget action, {
|
||||
BaseAction action, {
|
||||
List<Override> overrides = const [],
|
||||
}) async {
|
||||
await pumpTestWidget(context, action, overrides: overrides);
|
||||
await pumpTestWidget(context, ActionIconButtonWidget(action: action), overrides: overrides);
|
||||
await tap(find.byType(ImmichIconButton));
|
||||
await pump();
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ program
|
||||
.usage('[paths...] [options]')
|
||||
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
|
||||
.addOption(new Option('-i, --ignore <pattern>', 'Pattern to ignore').env('IMMICH_IGNORE_PATHS'))
|
||||
.addOption(new Option('--skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
|
||||
.addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
|
||||
.addOption(new Option('-H, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false))
|
||||
.addOption(
|
||||
new Option('-a, --album', 'Automatically create albums based on folder name')
|
||||
|
||||
@@ -308,76 +308,6 @@
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
},
|
||||
{
|
||||
"name": "assetExifFilter",
|
||||
"title": "Filter by EXIF metadata",
|
||||
"description": "Filter assets by their EXIF properties",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"property": {
|
||||
"title": "Property",
|
||||
"description": "EXIF property to match",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"make",
|
||||
"model",
|
||||
"exifImageWidth",
|
||||
"exifImageHeight",
|
||||
"fileSizeInByte",
|
||||
"orientation",
|
||||
"lensModel",
|
||||
"fNumber",
|
||||
"focalLength",
|
||||
"iso",
|
||||
"description",
|
||||
"fps",
|
||||
"exposureTime",
|
||||
"livePhotoCID",
|
||||
"timeZone",
|
||||
"projectionType",
|
||||
"profileDescription",
|
||||
"colorspace",
|
||||
"bitsPerSample",
|
||||
"rating"
|
||||
],
|
||||
"uiHint": {
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"title": "Pattern",
|
||||
"description": "Text or regex pattern to match against property value",
|
||||
"uiHint": {
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"title": "Match type",
|
||||
"enum": ["contains", "startsWith", "exact", "regex"],
|
||||
"default": "contains",
|
||||
"description": "Type of pattern matching to perform",
|
||||
"uiHint": {
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Case sensitive",
|
||||
"description": "Whether matching should be case-sensitive",
|
||||
"uiHint": {
|
||||
"order": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["property", "pattern"]
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
},
|
||||
{
|
||||
"name": "assetArchive",
|
||||
"title": "Archive asset",
|
||||
|
||||
@@ -2,42 +2,6 @@ import { wrapper } from '@immich/plugin-sdk';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import type { Manifest } from '../dist/index.d.ts';
|
||||
|
||||
type MatchValueConfig = {
|
||||
pattern: string;
|
||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
|
||||
caseSensitive?: boolean;
|
||||
};
|
||||
|
||||
const matchValueResult = (value: string, config: MatchValueConfig) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
const searchName = caseSensitive ? value : value.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
switch (matchType) {
|
||||
case 'contains': {
|
||||
return { workflow: { continue: searchName.includes(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'exact': {
|
||||
return { workflow: { continue: searchName === searchPattern } };
|
||||
}
|
||||
|
||||
case 'startsWith': {
|
||||
return { workflow: { continue: searchName.startsWith(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'regex': {
|
||||
const flags = caseSensitive ? '' : 'i';
|
||||
const regex = new RegExp(searchPattern, flags);
|
||||
return { workflow: { continue: regex.test(value) } };
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const methods = wrapper<Manifest>({
|
||||
assetAddToAlbums: ({ config, data, functions }) => {
|
||||
const assetId = data.asset.id;
|
||||
@@ -89,7 +53,39 @@ const methods = wrapper<Manifest>({
|
||||
}
|
||||
},
|
||||
|
||||
assetFileFilter: ({ data, config }) => matchValueResult(data.asset.originalFileName || '', config),
|
||||
assetFileFilter: ({ data, config }) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false, usePath = false } = config;
|
||||
|
||||
const { asset } = data;
|
||||
|
||||
const fileName = usePath ? asset.originalPath : asset.originalFileName;
|
||||
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
switch (matchType) {
|
||||
case 'contains': {
|
||||
return { workflow: { continue: searchName.includes(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'exact': {
|
||||
return { workflow: { continue: searchName === searchPattern } };
|
||||
}
|
||||
|
||||
case 'startsWith': {
|
||||
return { workflow: { continue: searchName.startsWith(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'regex': {
|
||||
const flags = caseSensitive ? '' : 'i';
|
||||
const regex = new RegExp(searchPattern, flags);
|
||||
return { workflow: { continue: regex.test(fileName) } };
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
assetLocationFilter: ({ config, data }) => {
|
||||
if (
|
||||
@@ -128,14 +124,6 @@ const methods = wrapper<Manifest>({
|
||||
return { workflow: { continue: earthDiameter * delta <= (config.coordinate?.radius ?? 0) } };
|
||||
},
|
||||
|
||||
assetExifFilter: ({ config, data }) => {
|
||||
if (!data.asset.exifInfo || data.asset.exifInfo[config.property] === null) {
|
||||
return { workflow: { continue: false } };
|
||||
}
|
||||
|
||||
return matchValueResult(String(data.asset.exifInfo[config.property]), config);
|
||||
},
|
||||
|
||||
assetDateFilter: ({ config, data }) => {
|
||||
const assetDate = new Date(data.asset.localDateTime);
|
||||
let startDate = new Date(config.startDate.year, config.startDate.month - 1, config.startDate.day);
|
||||
@@ -212,7 +200,6 @@ const {
|
||||
assetFavorite,
|
||||
assetFileFilter,
|
||||
assetLocationFilter,
|
||||
assetExifFilter,
|
||||
assetDateFilter,
|
||||
assetLock,
|
||||
assetMissingTimeZoneFilter,
|
||||
@@ -230,7 +217,6 @@ export {
|
||||
assetFavorite,
|
||||
assetFileFilter,
|
||||
assetLocationFilter,
|
||||
assetExifFilter,
|
||||
assetDateFilter,
|
||||
assetLock,
|
||||
assetMissingTimeZoneFilter,
|
||||
|
||||
352
pnpm-lock.yaml
generated
352
pnpm-lock.yaml
generated
@@ -7,7 +7,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
canvas: 3.2.3
|
||||
sharp: ^0.34.5
|
||||
sharp: ^0.35.2
|
||||
|
||||
packageExtensionsChecksum: sha256-W6pFzyf+6QXnV91iA6oob0OGVkergPXDN1afLgoF53k=
|
||||
|
||||
@@ -181,8 +181,8 @@ importers:
|
||||
specifier: ^4.0.0
|
||||
version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5)
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
specifier: ^0.35.2
|
||||
version: 0.35.2
|
||||
socket.io-client:
|
||||
specifier: ^4.7.4
|
||||
version: 4.8.3
|
||||
@@ -471,7 +471,7 @@ importers:
|
||||
version: 2.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@socket.io/redis-adapter':
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(socket.io-adapter@2.5.7)
|
||||
version: 8.3.0(socket.io-adapter@2.5.8)
|
||||
archiver:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.1
|
||||
@@ -611,8 +611,8 @@ importers:
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.5
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
specifier: ^0.35.2
|
||||
version: 0.35.2
|
||||
sirv:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.2
|
||||
@@ -1800,8 +1800,8 @@ packages:
|
||||
'@codemirror/lint@6.9.7':
|
||||
resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==}
|
||||
|
||||
'@codemirror/search@6.7.0':
|
||||
resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==}
|
||||
'@codemirror/search@6.7.1':
|
||||
resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==}
|
||||
|
||||
'@codemirror/state@6.6.0':
|
||||
resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==}
|
||||
@@ -2954,152 +2954,161 @@ packages:
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-darwin-arm64@0.35.2':
|
||||
resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-darwin-x64@0.35.2':
|
||||
resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
'@img/sharp-freebsd-wasm32@0.35.2':
|
||||
resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
os: [freebsd]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.1':
|
||||
resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
'@img/sharp-libvips-darwin-x64@1.3.1':
|
||||
resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
'@img/sharp-libvips-linux-arm64@1.3.1':
|
||||
resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
'@img/sharp-libvips-linux-arm@1.3.1':
|
||||
resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.1':
|
||||
resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.1':
|
||||
resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
'@img/sharp-libvips-linux-s390x@1.3.1':
|
||||
resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
'@img/sharp-libvips-linux-x64@1.3.1':
|
||||
resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.1':
|
||||
resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.1':
|
||||
resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linux-arm64@0.35.2':
|
||||
resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linux-arm@0.35.2':
|
||||
resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linux-ppc64@0.35.2':
|
||||
resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linux-riscv64@0.35.2':
|
||||
resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linux-s390x@0.35.2':
|
||||
resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linux-x64@0.35.2':
|
||||
resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linuxmusl-arm64@0.35.2':
|
||||
resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-linuxmusl-x64@0.35.2':
|
||||
resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-wasm32@0.35.2':
|
||||
resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.2':
|
||||
resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-win32-arm64@0.35.2':
|
||||
resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-win32-ia32@0.35.2':
|
||||
resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==}
|
||||
engines: {node: ^20.9.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
'@img/sharp-win32-x64@0.35.2':
|
||||
resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -7426,15 +7435,15 @@ packages:
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
engine.io-client@6.6.5:
|
||||
resolution: {integrity: sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==}
|
||||
engine.io-client@6.6.6:
|
||||
resolution: {integrity: sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==}
|
||||
|
||||
engine.io-parser@5.2.3:
|
||||
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
engine.io@6.6.8:
|
||||
resolution: {integrity: sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==}
|
||||
engine.io@6.6.9:
|
||||
resolution: {integrity: sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==}
|
||||
engines: {node: '>=10.2.0'}
|
||||
|
||||
enhanced-resolve@5.21.6:
|
||||
@@ -8458,8 +8467,8 @@ packages:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
import-in-the-middle@3.0.2:
|
||||
resolution: {integrity: sha512-LGLYRl0A2gtyUJb2WDliBHmk6TtlHwdDjxonacZ8QrEs/ZW+YDgNv2QAfjRQWpS8HqvNcq6GGnN6jrOa5FysDQ==}
|
||||
import-in-the-middle@3.1.0:
|
||||
resolution: {integrity: sha512-c0AeAV8VcwZzfYE7euTZY3H+VXUPMVugiovdosq80lqEXJmOekg3zGUAYg6KImHMaMuBoTUfTv7xNpUFdy0hJA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
import-lazy@4.0.0:
|
||||
@@ -11461,9 +11470,9 @@ packages:
|
||||
resolution: {integrity: sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
sharp@0.35.2:
|
||||
resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
@@ -11547,8 +11556,8 @@ packages:
|
||||
snake-case@3.0.4:
|
||||
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||
|
||||
socket.io-adapter@2.5.7:
|
||||
resolution: {integrity: sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==}
|
||||
socket.io-adapter@2.5.8:
|
||||
resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==}
|
||||
|
||||
socket.io-client@4.8.3:
|
||||
resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
|
||||
@@ -12327,8 +12336,8 @@ packages:
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
undici@6.26.0:
|
||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
||||
undici@6.27.0:
|
||||
resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@8.7.0:
|
||||
@@ -12913,8 +12922,8 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
ws@8.20.1:
|
||||
resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==}
|
||||
ws@8.21.0:
|
||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@@ -14010,7 +14019,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.7
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/parser': 7.27.0
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
debug: 4.4.3
|
||||
@@ -14079,7 +14088,7 @@ snapshots:
|
||||
'@codemirror/view': 6.43.1
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/search@6.7.0':
|
||||
'@codemirror/search@6.7.1':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.43.1
|
||||
@@ -15908,98 +15917,108 @@ snapshots:
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
'@img/sharp-darwin-arm64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
'@img/sharp-darwin-x64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
'@img/sharp-freebsd-wasm32@0.35.2':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.2
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
'@img/sharp-libvips-darwin-x64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
'@img/sharp-libvips-linux-arm64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
'@img/sharp-libvips-linux-arm@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
'@img/sharp-libvips-linux-s390x@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
'@img/sharp-libvips-linux-x64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.1':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
'@img/sharp-linux-arm@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
'@img/sharp-linux-ppc64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
'@img/sharp-linux-riscv64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
'@img/sharp-linux-s390x@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
'@img/sharp-linux-x64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
'@img/sharp-linuxmusl-arm64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
'@img/sharp-linuxmusl-x64@0.35.2':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
'@img/sharp-wasm32@0.35.2':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
'@img/sharp-webcontainers-wasm32@0.35.2':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.2
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
'@img/sharp-win32-arm64@0.35.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
'@img/sharp-win32-ia32@0.35.2':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.35.2':
|
||||
optional: true
|
||||
|
||||
'@immich/justified-layout-wasm@0.4.3': {}
|
||||
@@ -16914,7 +16933,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/api-logs': 0.220.0
|
||||
import-in-the-middle: 3.0.2
|
||||
import-in-the-middle: 3.1.0
|
||||
require-in-the-middle: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -17583,11 +17602,11 @@ snapshots:
|
||||
|
||||
'@socket.io/component-emitter@3.1.2': {}
|
||||
|
||||
'@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.7)':
|
||||
'@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.8)':
|
||||
dependencies:
|
||||
debug: 4.3.7
|
||||
notepack.io: 3.0.1
|
||||
socket.io-adapter: 2.5.7
|
||||
socket.io-adapter: 2.5.8
|
||||
uid2: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -17608,7 +17627,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))
|
||||
magic-string: 0.30.21
|
||||
sharp: 0.34.5
|
||||
sharp: 0.35.2
|
||||
svelte: 5.56.5(@typescript-eslint/types@8.64.0)
|
||||
svelte-parse-markup: 0.1.5(svelte@5.56.5(@typescript-eslint/types@8.64.0))
|
||||
vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)
|
||||
@@ -18368,7 +18387,7 @@ snapshots:
|
||||
|
||||
'@types/pg-pool@2.0.7':
|
||||
dependencies:
|
||||
'@types/pg': 8.20.0
|
||||
'@types/pg': 8.15.6
|
||||
|
||||
'@types/pg@8.15.6':
|
||||
dependencies:
|
||||
@@ -20501,12 +20520,12 @@ snapshots:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
engine.io-client@6.6.5:
|
||||
engine.io-client@6.6.6:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.4.3
|
||||
engine.io-parser: 5.2.3
|
||||
ws: 8.20.1
|
||||
ws: 8.21.0
|
||||
xmlhttprequest-ssl: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -20515,7 +20534,7 @@ snapshots:
|
||||
|
||||
engine.io-parser@5.2.3: {}
|
||||
|
||||
engine.io@6.6.8:
|
||||
engine.io@6.6.9:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.19
|
||||
'@types/node': 24.13.3
|
||||
@@ -20526,7 +20545,7 @@ snapshots:
|
||||
cors: 2.8.6
|
||||
debug: 4.4.3
|
||||
engine.io-parser: 5.2.3
|
||||
ws: 8.20.1
|
||||
ws: 8.21.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
@@ -21901,7 +21920,7 @@ snapshots:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
import-in-the-middle@3.0.2:
|
||||
import-in-the-middle@3.1.0:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn-import-attributes: 1.9.5(acorn@8.17.0)
|
||||
@@ -23509,7 +23528,7 @@ snapshots:
|
||||
semver: 7.8.5
|
||||
tar: 7.5.16
|
||||
tinyglobby: 0.2.17
|
||||
undici: 6.26.0
|
||||
undici: 6.27.0
|
||||
which: 7.0.0
|
||||
|
||||
node-releases@2.0.51: {}
|
||||
@@ -25319,7 +25338,7 @@ snapshots:
|
||||
stream-source: 0.3.5
|
||||
text-encoding: 0.6.4
|
||||
|
||||
sharp@0.34.5:
|
||||
sharp@0.35.2:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
@@ -25327,30 +25346,31 @@ snapshots:
|
||||
node-gyp: 13.0.0
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
'@img/sharp-darwin-arm64': 0.35.2
|
||||
'@img/sharp-darwin-x64': 0.35.2
|
||||
'@img/sharp-freebsd-wasm32': 0.35.2
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.1
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.1
|
||||
'@img/sharp-libvips-linux-arm': 1.3.1
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.1
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.1
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.1
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.1
|
||||
'@img/sharp-libvips-linux-x64': 1.3.1
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.1
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.1
|
||||
'@img/sharp-linux-arm': 0.35.2
|
||||
'@img/sharp-linux-arm64': 0.35.2
|
||||
'@img/sharp-linux-ppc64': 0.35.2
|
||||
'@img/sharp-linux-riscv64': 0.35.2
|
||||
'@img/sharp-linux-s390x': 0.35.2
|
||||
'@img/sharp-linux-x64': 0.35.2
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.2
|
||||
'@img/sharp-linuxmusl-x64': 0.35.2
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.2
|
||||
'@img/sharp-win32-arm64': 0.35.2
|
||||
'@img/sharp-win32-ia32': 0.35.2
|
||||
'@img/sharp-win32-x64': 0.35.2
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
@@ -25442,10 +25462,10 @@ snapshots:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.8.1
|
||||
|
||||
socket.io-adapter@2.5.7:
|
||||
socket.io-adapter@2.5.8:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
ws: 8.20.1
|
||||
ws: 8.21.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
@@ -25455,7 +25475,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.4.3
|
||||
engine.io-client: 6.6.5
|
||||
engine.io-client: 6.6.6
|
||||
socket.io-parser: 4.2.6
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -25475,8 +25495,8 @@ snapshots:
|
||||
base64id: 2.0.0
|
||||
cors: 2.8.6
|
||||
debug: 4.4.3
|
||||
engine.io: 6.6.8
|
||||
socket.io-adapter: 2.5.7
|
||||
engine.io: 6.6.9
|
||||
socket.io-adapter: 2.5.8
|
||||
socket.io-parser: 4.2.6
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -25779,7 +25799,7 @@ snapshots:
|
||||
'@codemirror/lang-json': 6.0.2
|
||||
'@codemirror/language': 6.12.3
|
||||
'@codemirror/lint': 6.9.7
|
||||
'@codemirror/search': 6.7.0
|
||||
'@codemirror/search': 6.7.1
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.43.1
|
||||
'@fortawesome/free-regular-svg-icons': 7.2.0
|
||||
@@ -26339,7 +26359,7 @@ snapshots:
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
undici@6.26.0: {}
|
||||
undici@6.27.0: {}
|
||||
|
||||
undici@8.7.0: {}
|
||||
|
||||
@@ -26562,7 +26582,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.4.0(rollup@4.62.0)
|
||||
imagetools-core: 9.1.0
|
||||
sharp: 0.34.5
|
||||
sharp: 0.35.2
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
@@ -27057,7 +27077,7 @@ snapshots:
|
||||
|
||||
ws@7.5.12: {}
|
||||
|
||||
ws@8.20.1: {}
|
||||
ws@8.21.0: {}
|
||||
|
||||
ws@8.21.1: {}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ allowBuilds:
|
||||
postman-code-generators: false
|
||||
overrides:
|
||||
canvas: 3.2.3
|
||||
sharp: ^0.34.5
|
||||
sharp: ^0.35.2
|
||||
packageExtensions:
|
||||
nestjs-kysely:
|
||||
dependencies:
|
||||
|
||||
@@ -20,8 +20,9 @@ RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \
|
||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter immich build && \
|
||||
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --prod --no-optional deploy /output/server-pruned
|
||||
pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter immich build && \
|
||||
pnpm --filter immich --prod deploy /output/server-pruned && \
|
||||
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --dir /output/server-pruned/node_modules/sharp exec npm run build
|
||||
|
||||
FROM builder AS web
|
||||
|
||||
@@ -37,7 +38,7 @@ RUN --mount=type=cache,id=pnpm-web,target=/buildcache/pnpm-store \
|
||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter immich-web install --frozen-lockfile --force && \
|
||||
pnpm --filter @immich/sdk --filter immich-web install --frozen-lockfile --force && \
|
||||
pnpm --filter @immich/sdk --filter immich-web build
|
||||
|
||||
FROM builder AS cli
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.8.1",
|
||||
"sharp": "^0.34.5",
|
||||
"sharp": "^0.35.2",
|
||||
"sirv": "^3.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"tailwindcss-preset-email": "^1.4.0",
|
||||
@@ -169,6 +169,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.34.5"
|
||||
"sharp": "^0.35.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
} from 'src/commands/media-location.command';
|
||||
import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
|
||||
import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
|
||||
import { PromptPasswordResetQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
||||
import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
||||
import { SchemaCheck } from 'src/commands/schema-check';
|
||||
import { VersionCommand } from 'src/commands/version.command';
|
||||
|
||||
export const commandsAndQuestions = [
|
||||
ResetAdminPasswordCommand,
|
||||
PromptPasswordResetQuestions,
|
||||
PromptPasswordQuestions,
|
||||
PromptEmailQuestion,
|
||||
EnablePasswordLoginCommand,
|
||||
DisablePasswordLoginCommand,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
||||
const prompt = (inquirer: InquirerService) => {
|
||||
return (admin: UserAdminResponseDto) => {
|
||||
return function ask(admin: UserAdminResponseDto) {
|
||||
const { id, oauthId, email, name } = admin;
|
||||
console.log(`Found Admin:
|
||||
- ID=${id}
|
||||
@@ -11,7 +11,7 @@ const prompt = (inquirer: InquirerService) => {
|
||||
- Email=${email}
|
||||
- Name=${name}`);
|
||||
|
||||
return inquirer.ask<{ newPassword: string; invalidateSessions: boolean }>('prompt-password-reset', {});
|
||||
return inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -43,23 +43,13 @@ export class ResetAdminPasswordCommand extends CommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
@QuestionSet({ name: 'prompt-password-reset' })
|
||||
export class PromptPasswordResetQuestions {
|
||||
@QuestionSet({ name: 'prompt-password' })
|
||||
export class PromptPasswordQuestions {
|
||||
@Question({
|
||||
message: 'Please choose a new password (optional)',
|
||||
name: 'newPassword',
|
||||
name: 'password',
|
||||
})
|
||||
parsePassword(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Question({
|
||||
type: 'confirm',
|
||||
message: 'Invalidate existing sessions?',
|
||||
default: true,
|
||||
name: 'invalidateSessions',
|
||||
})
|
||||
parseInvalidate(value: boolean): boolean {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,34 +568,6 @@ describe(AssetService.name, () => {
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith(asset.stackId);
|
||||
});
|
||||
|
||||
it('should delete the stack when a non-primary asset is deleted and only the primary would remain', async () => {
|
||||
const asset = AssetFactory.from().build();
|
||||
const deletionAsset = {
|
||||
...getForAssetDeletion(asset),
|
||||
stack: { id: newUuid(), primaryAssetId: newUuid(), assets: [{ id: asset.id }] },
|
||||
};
|
||||
mocks.stack.delete.mockResolvedValue();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(deletionAsset);
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith(deletionAsset.stack.id);
|
||||
});
|
||||
|
||||
it('should keep the stack when a non-primary asset is deleted and the primary plus another asset remain', async () => {
|
||||
const asset = AssetFactory.from().build();
|
||||
const deletionAsset = {
|
||||
...getForAssetDeletion(asset),
|
||||
stack: { id: newUuid(), primaryAssetId: newUuid(), assets: [{ id: asset.id }, { id: newUuid() }] },
|
||||
};
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(deletionAsset);
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
expect(mocks.stack.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete a live photo', async () => {
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
|
||||
@@ -316,25 +316,18 @@ export class AssetService extends BaseService {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
if (asset.stack) {
|
||||
// asset.stack.assets only includes timeline visible assets and excludes the primary asset
|
||||
const remainingStackAssetIds = asset.stack.assets.map((a) => a.id).filter((assetId) => assetId !== id);
|
||||
|
||||
// the primary survives unless it is the asset being deleted
|
||||
let remainingCount = remainingStackAssetIds.length;
|
||||
if (asset.stack.primaryAssetId !== id) {
|
||||
remainingCount++;
|
||||
}
|
||||
|
||||
if (remainingCount < 2) {
|
||||
// 0 or 1 asset would remain: dissolve the stack so it does not linger as a single-asset stack
|
||||
await this.stackRepository.delete(asset.stack.id);
|
||||
} else if (asset.stack.primaryAssetId === id) {
|
||||
// the primary is being deleted but others remain: promote a new primary
|
||||
// replace the parent of the stack children with a new asset
|
||||
if (asset.stack?.primaryAssetId === id) {
|
||||
// this only includes timeline visible assets and excludes the primary asset
|
||||
const stackAssetIds = asset.stack.assets.map((a) => a.id);
|
||||
if (stackAssetIds.length >= 2) {
|
||||
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
|
||||
await this.stackRepository.update(asset.stack.id, {
|
||||
id: asset.stack.id,
|
||||
primaryAssetId: remainingStackAssetIds[0],
|
||||
primaryAssetId: newPrimaryAssetId,
|
||||
});
|
||||
} else {
|
||||
await this.stackRepository.delete(asset.stack.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ describe(CliService.name, () => {
|
||||
mocks.user.getAdmin.mockResolvedValue(admin);
|
||||
mocks.user.update.mockResolvedValue(UserFactory.create({ isAdmin: true }));
|
||||
|
||||
const ask = vitest.fn().mockResolvedValue({ newPassword: undefined, invalidateSessions: false });
|
||||
const ask = vitest.fn().mockImplementation(() => {});
|
||||
|
||||
const response = await sut.resetAdminPassword(ask);
|
||||
|
||||
@@ -47,7 +47,6 @@ describe(CliService.name, () => {
|
||||
expect(ask).toHaveBeenCalled();
|
||||
expect(id).toEqual(admin.id);
|
||||
expect(update.password).toBeDefined();
|
||||
expect(mocks.session.invalidateAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use the supplied password', async () => {
|
||||
@@ -56,7 +55,7 @@ describe(CliService.name, () => {
|
||||
mocks.user.getAdmin.mockResolvedValue(admin);
|
||||
mocks.user.update.mockResolvedValue(admin);
|
||||
|
||||
const ask = vitest.fn().mockResolvedValue({ newPassword: 'new-password', invalidateSessions: false });
|
||||
const ask = vitest.fn().mockResolvedValue('new-password');
|
||||
|
||||
const response = await sut.resetAdminPassword(ask);
|
||||
|
||||
@@ -67,20 +66,6 @@ describe(CliService.name, () => {
|
||||
expect(id).toEqual(admin.id);
|
||||
expect(update.password).toBeDefined();
|
||||
});
|
||||
|
||||
it('should invalidate existing sessions when requested', async () => {
|
||||
const admin = UserFactory.create({ isAdmin: true });
|
||||
|
||||
mocks.user.getAdmin.mockResolvedValue(admin);
|
||||
mocks.user.update.mockResolvedValue(admin);
|
||||
mocks.session.invalidateAll.mockResolvedValue(void 0);
|
||||
|
||||
const ask = vitest.fn().mockResolvedValue({ newPassword: 'new-password', invalidateSessions: true });
|
||||
|
||||
await sut.resetAdminPassword(ask);
|
||||
|
||||
expect(mocks.session.invalidateAll).toHaveBeenCalledWith({ userId: admin.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('disablePasswordLogin', () => {
|
||||
|
||||
@@ -58,24 +58,18 @@ export class CliService extends BaseService {
|
||||
return users.map((user) => mapUserAdmin(user));
|
||||
}
|
||||
|
||||
async resetAdminPassword(
|
||||
ask: (admin: UserAdminResponseDto) => Promise<{ newPassword: string | undefined; invalidateSessions: boolean }>,
|
||||
) {
|
||||
async resetAdminPassword(ask: (admin: UserAdminResponseDto) => Promise<string | undefined>) {
|
||||
const admin = await this.userRepository.getAdmin();
|
||||
if (!admin) {
|
||||
throw new Error('Admin account does not exist');
|
||||
}
|
||||
|
||||
const { newPassword: providedPassword, invalidateSessions } = await ask(mapUserAdmin(admin));
|
||||
const providedPassword = await ask(mapUserAdmin(admin));
|
||||
const password = providedPassword || this.cryptoRepository.randomBytesAsText(24);
|
||||
const hashedPassword = await this.cryptoRepository.hashBcrypt(password, SALT_ROUNDS);
|
||||
|
||||
await this.userRepository.update(admin.id, { password: hashedPassword });
|
||||
|
||||
if (invalidateSessions) {
|
||||
await this.sessionRepository.invalidateAll({ userId: admin.id });
|
||||
}
|
||||
|
||||
return { admin, password, provided: !!providedPassword };
|
||||
}
|
||||
|
||||
|
||||
@@ -433,10 +433,9 @@ describe('core plugin', () => {
|
||||
describe('assetDateFilter', () => {
|
||||
it('should favorite assets created during the first 7 days of a specific year and month', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const [{ asset: asset1 }, { asset: asset2 }, { asset: asset3 }] = await Promise.all([
|
||||
const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([
|
||||
ctx.newAsset({ ownerId: user.id, localDateTime: new Date('2000-04-01') }),
|
||||
ctx.newAsset({ ownerId: user.id, localDateTime: new Date('2000-04-07T23:59:59Z') }),
|
||||
ctx.newAsset({ ownerId: user.id, localDateTime: new Date('2000-04-08T00:00:00Z') }),
|
||||
]);
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
@@ -462,46 +461,6 @@ describe('core plugin', () => {
|
||||
|
||||
await ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id });
|
||||
await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
|
||||
await ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset3.id });
|
||||
await expect(ctx.get(AssetRepository).getById(asset3.id)).resolves.toMatchObject({ isFavorite: false });
|
||||
});
|
||||
|
||||
it('should match recurring dates regardless of the year', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const [{ asset: asset1 }, { asset: asset2 }, { asset: asset3 }] = await Promise.all([
|
||||
ctx.newAsset({ ownerId: user.id, localDateTime: new Date('2026-03-01') }),
|
||||
ctx.newAsset({ ownerId: user.id, localDateTime: new Date('1998-12-21') }),
|
||||
ctx.newAsset({ ownerId: user.id, localDateTime: new Date('2000-04-08T00:00:00Z') }),
|
||||
]);
|
||||
await ctx.newAsset({ ownerId: user.id, localDateTime: new Date('2010-06-15') });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetDateFilter',
|
||||
config: {
|
||||
startDate: { day: 12, month: 12, year: 2000 },
|
||||
endDate: { day: 30, month: 3, year: 2001 },
|
||||
recurring: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset1.id });
|
||||
await expect(ctx.get(AssetRepository).getById(asset1.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
|
||||
await ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id });
|
||||
await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
|
||||
await ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset3.id });
|
||||
await expect(ctx.get(AssetRepository).getById(asset3.id)).resolves.toMatchObject({ isFavorite: false });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,36 +55,34 @@
|
||||
{/each}
|
||||
</div> -->
|
||||
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<div class="grid grid-flow-col grid-rows-7 gap-0.5">
|
||||
<div class="row-span-7 grid grid-rows-subgrid">
|
||||
{#if Info.getStartOfWeek({ locale: $locale }) === 7}
|
||||
<div></div>
|
||||
{/if}
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[0]}</Text></div>
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[1]}</Text></div>
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[2]}</Text></div>
|
||||
{#if Info.getStartOfWeek({ locale: $locale }) === 1}
|
||||
<div class="-my-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[3]}</Text></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#each data.series as day, idx (day.date)}
|
||||
{@const date = DateTime.fromISO(day.date, { zone: 'utc' }).toLocaleString(
|
||||
{ month: 'short', day: 'numeric' },
|
||||
{ locale: $locale },
|
||||
)}
|
||||
<div
|
||||
class="aspect-square size-full rounded-sm {itemColors(day.count)} row-start-(--heatmap-row-start)"
|
||||
style:--heatmap-row-start={idx === 0 ? padding + 1 : undefined}
|
||||
title={itemLabel({ date, count: day.count })}
|
||||
aria-label={itemLabel({ date, count: day.count })}
|
||||
></div>
|
||||
{/each}
|
||||
<div class="grid grid-flow-col grid-rows-7 gap-0.5">
|
||||
<div class="row-span-7 grid grid-rows-subgrid">
|
||||
{#if Info.getStartOfWeek({ locale: $locale }) === 7}
|
||||
<div></div>
|
||||
{/if}
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[0]}</Text></div>
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[1]}</Text></div>
|
||||
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[2]}</Text></div>
|
||||
{#if Info.getStartOfWeek({ locale: $locale }) === 1}
|
||||
<div class="-my-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[3]}</Text></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#each data.series as day, idx (day.date)}
|
||||
{@const date = DateTime.fromISO(day.date, { zone: 'utc' }).toLocaleString(
|
||||
{ month: 'short', day: 'numeric' },
|
||||
{ locale: $locale },
|
||||
)}
|
||||
<div
|
||||
class="aspect-square size-full rounded-sm {itemColors(day.count)} row-start-(--heatmap-row-start)"
|
||||
style:--heatmap-row-start={idx === 0 ? padding + 1 : undefined}
|
||||
title={itemLabel({ date, count: day.count })}
|
||||
aria-label={itemLabel({ date, count: day.count })}
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div class="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{$t('less')}</span>
|
||||
<span class="size-3 rounded-sm bg-gray-200 dark:bg-gray-700"></span>
|
||||
<span class="size-3 rounded-sm bg-immich-primary/30"></span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AssetOrder, AssetOrderBy, type AssetResponseDto } from '@immich/sdk';
|
||||
import { AssetOrder, type AssetResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getOrderingDate, plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { TimelineManager } from '../timeline-manager.svelte';
|
||||
import type { TimelineMonth } from '../timeline-month.svelte';
|
||||
import type { AssetDescriptor, Direction, TimelineAsset } from '../types';
|
||||
@@ -125,14 +125,7 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass
|
||||
return [];
|
||||
}
|
||||
const assetOrder: AssetOrder = timelineManager.getAssetOrder();
|
||||
const orderBy: AssetOrderBy = timelineManager.getOrderBy();
|
||||
if (
|
||||
plainDateTimeCompare(
|
||||
assetOrder === AssetOrder.Desc,
|
||||
getOrderingDate(startAsset, orderBy),
|
||||
getOrderingDate(endAsset, orderBy),
|
||||
) < 0
|
||||
) {
|
||||
if (plainDateTimeCompare(assetOrder === AssetOrder.Desc, startAsset.localDateTime, endAsset.localDateTime) < 0) {
|
||||
[startAsset, endAsset] = [endAsset, startAsset];
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
[startTimelineMonth, endTimelineMonth] = [endTimelineMonth, startTimelineMonth];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetOrderBy, AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||
import { AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||
import { tick } from 'svelte';
|
||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
@@ -808,44 +808,4 @@ describe('TimelineManager', () => {
|
||||
expect(b.showAssetOwners).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('retrieveRange', () => {
|
||||
it('uses createdAt ordering in the Recently Added view (orderBy=CreatedAt)', async () => {
|
||||
// Simulate the "Recently Added" bug: two assets whose localDateTime order is
|
||||
// the reverse of their createdAt (upload) order. Before the fix, retrieveRange
|
||||
// compared localDateTime and selected the wrong range direction.
|
||||
const timelineManager = new TimelineManager();
|
||||
sdkMock.getTimeBuckets.mockResolvedValue([]);
|
||||
await timelineManager.updateOptions({ orderBy: AssetOrderBy.CreatedAt });
|
||||
|
||||
// assetA was taken recently (2024) but uploaded first (2025-01)
|
||||
const assetA = timelineAssetFactory.build({
|
||||
localDateTime: fromISODateTimeUTCToObject('2024-06-01T00:00:00.000Z'),
|
||||
createdAt: fromISODateTimeUTCToObject('2025-01-20T00:00:00.000Z'),
|
||||
fileCreatedAt: fromISODateTimeUTCToObject('2025-01-20T00:00:00.000Z'),
|
||||
});
|
||||
// assetB was taken long ago (2018) but uploaded second (2025-02)
|
||||
const assetB = timelineAssetFactory.build({
|
||||
localDateTime: fromISODateTimeUTCToObject('2018-03-15T00:00:00.000Z'),
|
||||
createdAt: fromISODateTimeUTCToObject('2025-02-10T00:00:00.000Z'),
|
||||
fileCreatedAt: fromISODateTimeUTCToObject('2025-02-10T00:00:00.000Z'),
|
||||
});
|
||||
// assetC is an unrelated asset that should not appear in the selection
|
||||
const assetC = timelineAssetFactory.build({
|
||||
localDateTime: fromISODateTimeUTCToObject('2022-01-01T00:00:00.000Z'),
|
||||
createdAt: fromISODateTimeUTCToObject('2025-03-01T00:00:00.000Z'),
|
||||
fileCreatedAt: fromISODateTimeUTCToObject('2025-03-01T00:00:00.000Z'),
|
||||
});
|
||||
|
||||
timelineManager.upsertAssets([assetA, assetB, assetC]);
|
||||
|
||||
// Shift-click from assetB (uploaded most recently) to assetA — the range
|
||||
// between them in the "Recently Added" timeline should contain only those two.
|
||||
const range = await timelineManager.retrieveRange({ id: assetB.id }, { id: assetA.id });
|
||||
const ids = range.map((a) => a.id);
|
||||
expect(ids).toContain(assetA.id);
|
||||
expect(ids).toContain(assetB.id);
|
||||
expect(ids).not.toContain(assetC.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -625,10 +625,6 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
return this.#options.order ?? AssetOrder.Desc;
|
||||
}
|
||||
|
||||
getOrderBy() {
|
||||
return this.#options.orderBy ?? AssetOrderBy.TakenAt;
|
||||
}
|
||||
|
||||
protected postCreateSegments(): void {
|
||||
this.months.sort((a, b) => {
|
||||
return a.yearMonth.year === b.yearMonth.year
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
</Stack>
|
||||
</AdminCard>
|
||||
|
||||
<div class="col-span-full px-4 py-2">
|
||||
<div class="col-span-2 px-4 py-2">
|
||||
<div class="flex gap-2 text-primary">
|
||||
<Icon icon={mdiCloudUploadOutline} size="1.5rem" />
|
||||
<CardTitle>{$t('uploads')}</CardTitle>
|
||||
|
||||
Reference in New Issue
Block a user