mirror of
https://github.com/immich-app/immich.git
synced 2026-06-16 20:02:15 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba3e0560ed | |||
| 42782bf842 |
@@ -237,7 +237,7 @@ jobs:
|
||||
run: flutter build ios --config-only --no-codesign
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@12fd324f1d0b43274fdc8130f6980590a667c455 # v1.312.0
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@3530478ec30f84adedbfeb28f0d9527a290f50a9 # v0.0.57
|
||||
uses: oasdiff/oasdiff-action/breaking@a8c7f0e5649d20d623edb5b38446d3ab3d82d43c # v0.0.53
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:2a64c7f045cb7b580fbdf3614d7d1805f5775fec453e3d1023764180efa8c70b
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:e73f60195b39748c4876f23e3e6cd22a68a9754acec8aef1fd6979fd52cd2c9f
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
suffixes: '-rocm'
|
||||
platforms: linux/amd64
|
||||
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
name: Build and Push Server
|
||||
needs: pre-job
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
+1
-4
@@ -1,8 +1,5 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.wrangler]]
|
||||
version = "4.98.0"
|
||||
version = "4.66.0"
|
||||
backend = "npm:wrangler"
|
||||
|
||||
[tools.wrangler.options]
|
||||
allow_builds = '["esbuild", "sharp", "workerd"]'
|
||||
|
||||
+2
-1
@@ -32,7 +32,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.12.4",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^7.0.0",
|
||||
@@ -54,6 +54,7 @@
|
||||
"typescript": "^6.0.0",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"utimes": "^5.2.1",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true';
|
||||
@@ -23,7 +24,5 @@ export default defineConfig({
|
||||
maxWorkers: 1,
|
||||
isolate: false,
|
||||
},
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
plugins: [tsconfigPaths()],
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true';
|
||||
@@ -23,7 +24,5 @@ export default defineConfig({
|
||||
maxWorkers: 1,
|
||||
isolate: false,
|
||||
},
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
plugins: [tsconfigPaths()],
|
||||
});
|
||||
|
||||
@@ -189,23 +189,18 @@
|
||||
"machine_learning_smart_search_enabled": "Enable smart search",
|
||||
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
||||
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
||||
"maintenance_backup_management": "Backup management",
|
||||
"maintenance_delete_backup": "Delete Backup",
|
||||
"maintenance_delete_backup_description": "This file will be irrevocably deleted.",
|
||||
"maintenance_delete_error": "Failed to delete backup.",
|
||||
"maintenance_integrity_check": "Check",
|
||||
"maintenance_integrity_check_all": "Check All",
|
||||
"maintenance_integrity_checksum_mismatch": "Checksum Mismatch",
|
||||
"maintenance_integrity_checksum_mismatch_description": "Files whose on-disk checksum mismatches the checksum Immich has stored in its database.",
|
||||
"maintenance_integrity_checksum_mismatch_job": "Check for checksum mismatches",
|
||||
"maintenance_integrity_checksum_mismatch_refresh_job": "Refresh checksum mismatch reports",
|
||||
"maintenance_integrity_missing_file": "Missing Files",
|
||||
"maintenance_integrity_missing_file_description": "Files that Immich has tracked in its database but do not exist on the file system.",
|
||||
"maintenance_integrity_missing_file_job": "Check for missing files",
|
||||
"maintenance_integrity_missing_file_refresh_job": "Refresh missing file reports",
|
||||
"maintenance_integrity_report": "Integrity Report",
|
||||
"maintenance_integrity_untracked_file": "Untracked Files",
|
||||
"maintenance_integrity_untracked_file_description": "Files in Immich's directories that Immich does not have any record of.",
|
||||
"maintenance_integrity_untracked_file_job": "Check for untracked files",
|
||||
"maintenance_integrity_untracked_file_refresh_job": "Refresh untracked file reports",
|
||||
"maintenance_restore_backup": "Restore Backup",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:6fe9bb8b5e309a4fc598acb9d3ad18718f7519911eb6af9a1d9121d960f0ed75 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:20ec607c68642c64c73269ce245aa0f060913f4129d15d9687850aa158e6269c AS builder-cpu
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:c33f0bc4364a6881bed1ec0cc2665e6c53c87a43e774aaeab88e6f17af105e4f AS builder-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS builder-openvino
|
||||
|
||||
FROM builder-cpu AS builder-cuda
|
||||
|
||||
@@ -44,7 +44,7 @@ FROM python:3.11-slim-bookworm@sha256:e2d3af735aff6eeee600b1933bedd99da6645fedf5
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:c33f0bc4364a6881bed1ec0cc2665e6c53c87a43e774aaeab88e6f17af105e4f AS prod-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
|
||||
Generated
+21
-21
@@ -2528,7 +2528,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "rapidocr"
|
||||
version = "3.8.4"
|
||||
version = "3.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorlog" },
|
||||
@@ -2544,7 +2544,7 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/26/387eca0137a3507ea62acedd2b2903697223cbca2f8561f7c63b47ec3ad2/rapidocr-3.8.4-py3-none-any.whl", hash = "sha256:1a8400df99ea2348a6d1902d1c6fa64c29edcf56b3c58cecd4cf1e5ff51b7ae2", size = 15018555, upload-time = "2026-06-15T08:55:07.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/4a/fa521d947f0fc7bb304bf11bec4cb66266bd81494588b4cb48dc01001719/rapidocr-3.8.1-py3-none-any.whl", hash = "sha256:650044b1fbce9e6bae5cae462dcf8be754cde11e2f23fc51f65dcc08deae2c46", size = 15080319, upload-time = "2026-04-11T07:13:22.56Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2638,27 +2638,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.17"
|
||||
version = "0.15.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -82,69 +82,6 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".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"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
|
||||
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||
|
||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
|
||||
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
|
||||
|
||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
|
||||
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
|
||||
|
||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg".options]
|
||||
asset_pattern = "jellyfin-ffmpeg_*_portable_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"
|
||||
@@ -208,33 +145,6 @@ url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c55847602
|
||||
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||
|
||||
[[tools.java]]
|
||||
version = "21.0.2"
|
||||
backend = "core:java"
|
||||
|
||||
[tools.java.options]
|
||||
shorthand_vendor = "openjdk"
|
||||
|
||||
[tools.java."platforms.linux-arm64"]
|
||||
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.linux-x64"]
|
||||
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.macos-arm64"]
|
||||
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.macos-x64"]
|
||||
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.windows-x64"]
|
||||
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||
|
||||
[[tools.node]]
|
||||
version = "24.15.0"
|
||||
backend = "core:node"
|
||||
|
||||
@@ -17,8 +17,7 @@ const Map<String, Locale> locales = {
|
||||
'Dutch (nl)': Locale('nl'),
|
||||
'English (United Kingdom) (en_GB)': Locale('en', 'GB'),
|
||||
'Estonian (et)': Locale('et'),
|
||||
'Filipino (fil)': Locale('fil'),
|
||||
'Filipino (Tagalog) (tl)': Locale('tl'),
|
||||
'Filipino (tl)': Locale('tl'),
|
||||
'Finnish (fi)': Locale('fi'),
|
||||
'French (fr)': Locale('fr'),
|
||||
'Galician (gl)': Locale('gl'),
|
||||
|
||||
@@ -104,7 +104,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
Future<void> onAndroidUpload(int? maxMinutes) async {
|
||||
final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||
final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
|
||||
await _optimizeDB();
|
||||
return _backgroundLoop(
|
||||
hashTimeout: hashTimeout,
|
||||
backupTimeout: backupTimeout,
|
||||
@@ -124,11 +123,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only for Background Processing tasks
|
||||
if (maxSeconds == null) {
|
||||
await _optimizeDB();
|
||||
}
|
||||
|
||||
// Run sync local, sync remote, hash and backup concurrently so the bg
|
||||
// refresh task (20s budget) can make progress on all four instead of
|
||||
// racing them sequentially. Phases are independent at the data layer:
|
||||
@@ -199,14 +193,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _optimizeDB() async {
|
||||
try {
|
||||
await (_drift.optimize(allTables: true), _driftLogger.optimize()).wait;
|
||||
} catch (error, stack) {
|
||||
dPrint(() => "Error during background worker optimize: $error, $stack");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanup() async {
|
||||
await runZonedGuarded(_handleCleanup, (error, stack) {
|
||||
dPrint(() => "Error during background worker cleanup: $error, $stack");
|
||||
@@ -235,7 +221,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
if (nativeSyncApi != null) nativeSyncApi.cancelHashing(),
|
||||
]);
|
||||
await workerManagerPatch.dispose().catchError((_) async {});
|
||||
await Future.wait([LogService.I.dispose(), Store.dispose()]);
|
||||
await Future.wait([LogService.I.dispose(), Store.dispose(), _drift.optimize(allTables: true)]);
|
||||
await _drift.close();
|
||||
await _driftLogger.close();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
@@ -18,6 +19,7 @@ final syncLinkedAlbumServiceProvider = Provider(
|
||||
ref.watch(remoteAlbumRepository),
|
||||
ref.watch(driftAlbumApiRepositoryProvider),
|
||||
ref.watch(storeServiceProvider),
|
||||
cancellation: ref.watch(cancellationProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -26,17 +28,19 @@ class SyncLinkedAlbumService {
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
final StoreService _storeService;
|
||||
final Completer<void>? _cancellation;
|
||||
|
||||
SyncLinkedAlbumService(
|
||||
this._localAlbumRepository,
|
||||
this._remoteAlbumRepository,
|
||||
this._albumApiRepository,
|
||||
this._storeService,
|
||||
);
|
||||
this._storeService, {
|
||||
this._cancellation,
|
||||
});
|
||||
|
||||
final _log = Logger("SyncLinkedAlbumService");
|
||||
|
||||
Future<void> syncLinkedAlbums(String userId, {Completer<void>? cancellation}) async {
|
||||
Future<void> syncLinkedAlbums(String userId) async {
|
||||
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
|
||||
await Future.wait(
|
||||
@@ -60,7 +64,7 @@ class SyncLinkedAlbumService {
|
||||
final album = await _albumApiRepository.addAssets(
|
||||
remoteAlbum.id,
|
||||
assetIds,
|
||||
abortTrigger: cancellation?.future,
|
||||
abortTrigger: _cancellation?.future,
|
||||
);
|
||||
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||
@@ -11,7 +10,5 @@ Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||
Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync");
|
||||
return Future.value();
|
||||
}
|
||||
return ref
|
||||
.read(syncLinkedAlbumServiceProvider)
|
||||
.syncLinkedAlbums(user.id, cancellation: ref.read(cancellationProvider));
|
||||
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
|
||||
import 'package:drift_sqlite_async/drift_sqlite_async.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.drift.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:sqlite_async/sqlite_async.dart';
|
||||
|
||||
@DriftDatabase(tables: [LogMessageEntity])
|
||||
@@ -14,14 +13,6 @@ class DriftLogger extends $DriftLogger {
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
Future<void> optimize() async {
|
||||
try {
|
||||
await customStatement('PRAGMA optimize=0x10002');
|
||||
} catch (error) {
|
||||
dPrint(() => 'Failed to optimize logger database: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
beforeOpen: (details) async {
|
||||
|
||||
@@ -6,12 +6,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
@@ -53,9 +51,7 @@ class _SharePreparingDialog extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ShareFileTypeDialog extends StatelessWidget {
|
||||
final bool showPreview;
|
||||
|
||||
const _ShareFileTypeDialog({this.showPreview = true});
|
||||
const _ShareFileTypeDialog();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -70,12 +66,11 @@ class _ShareFileTypeDialog extends StatelessWidget {
|
||||
title: Text(context.t.share_original),
|
||||
onTap: () => context.pop(ShareAssetType.original),
|
||||
),
|
||||
if (showPreview)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_size_select_large_rounded),
|
||||
title: Text(context.t.share_preview),
|
||||
onTap: () => context.pop(ShareAssetType.preview),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_size_select_large_rounded),
|
||||
title: Text(context.t.share_preview),
|
||||
onTap: () => context.pop(ShareAssetType.preview),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel))],
|
||||
@@ -90,16 +85,6 @@ class ShareActionButton extends ConsumerWidget {
|
||||
|
||||
const ShareActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Set<BaseAsset> _getSelectedAssets(WidgetRef ref) {
|
||||
return switch (source) {
|
||||
ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets,
|
||||
ActionSource.viewer => switch (ref.read(assetViewerProvider).currentAsset) {
|
||||
BaseAsset asset => {asset},
|
||||
null => const {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
@@ -114,14 +99,9 @@ class ShareActionButton extends ConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
// only show preview option when at least one of the assets is not a video
|
||||
// we cant share previews of videos
|
||||
final assets = _getSelectedAssets(ref);
|
||||
final showPreview = assets.isEmpty || assets.any((asset) => !asset.isVideo);
|
||||
|
||||
final fileType = await showDialog<ShareAssetType>(
|
||||
context: context,
|
||||
builder: (_) => _ShareFileTypeDialog(showPreview: showPreview),
|
||||
builder: (_) => const _ShareFileTypeDialog(),
|
||||
useRootNavigator: false,
|
||||
);
|
||||
|
||||
|
||||
@@ -23,9 +23,7 @@ class MapBottomSheet extends StatelessWidget {
|
||||
resizeOnScroll: false,
|
||||
actions: [],
|
||||
backgroundColor: context.themeData.colorScheme.surface,
|
||||
slivers: [
|
||||
const SliverFillRemaining(hasScrollBody: false, child: SizedBox(height: 0, child: _ScopedMapTimeline())),
|
||||
],
|
||||
slivers: [const SliverFillRemaining(hasScrollBody: true, child: _ScopedMapTimeline())],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,9 +285,7 @@ class AssetMediaRepository {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final effectiveFileType = asset.isVideo ? ShareAssetType.original : fileType;
|
||||
|
||||
final shareFile = switch (effectiveFileType) {
|
||||
final shareFile = switch (fileType) {
|
||||
ShareAssetType.original => await _getOriginalShareFile(
|
||||
asset,
|
||||
cancelCompleter: cancelCompleter,
|
||||
|
||||
@@ -64,76 +64,6 @@ checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e3158
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
|
||||
|
||||
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||
version = "1.37.0"
|
||||
backend = "github:CQLabs/homebrew-dcm"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm".options]
|
||||
asset_pattern = "dcm-linux-arm-release.zip"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
|
||||
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||
|
||||
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||
version = "1.37.0"
|
||||
backend = "github:CQLabs/homebrew-dcm"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm".options]
|
||||
asset_pattern = "dcm-linux-x64-release.zip"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
|
||||
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||
|
||||
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||
version = "1.37.0"
|
||||
backend = "github:CQLabs/homebrew-dcm"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm".options]
|
||||
asset_pattern = "dcm-macos-x64-release.zip"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
|
||||
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
|
||||
|
||||
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||
version = "1.37.0"
|
||||
backend = "github:CQLabs/homebrew-dcm"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm".options]
|
||||
asset_pattern = "dcm-windows-release.zip"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
|
||||
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
|
||||
|
||||
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||
version = "1.37.0"
|
||||
backend = "github:CQLabs/homebrew-dcm"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm".options]
|
||||
asset_pattern = "dcm-macos-arm-release.zip"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
|
||||
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
|
||||
|
||||
[[tools.java]]
|
||||
version = "21.0.2"
|
||||
backend = "core:java"
|
||||
@@ -157,30 +87,3 @@ url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c55847602
|
||||
[tools.java."platforms.windows-x64"]
|
||||
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||
|
||||
[[tools.java]]
|
||||
version = "21.0.2"
|
||||
backend = "core:java"
|
||||
|
||||
[tools.java.options]
|
||||
shorthand_vendor = "openjdk"
|
||||
|
||||
[tools.java."platforms.linux-arm64"]
|
||||
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.linux-x64"]
|
||||
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.macos-arm64"]
|
||||
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.macos-x64"]
|
||||
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.windows-x64"]
|
||||
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
Optional<String?> dateTimeOriginal;
|
||||
|
||||
/// Relative time offset in minutes
|
||||
/// Relative time offset in seconds
|
||||
///
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Maximum value: 9007199254740991
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ class AssetBulkUploadCheckItem {
|
||||
/// Base64 or hex encoded SHA1 hash
|
||||
String checksum;
|
||||
|
||||
/// Client-side identifier echoed in the response to match results to inputs (e.g. filename)
|
||||
/// Asset ID
|
||||
String id;
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
import '../service.mock.dart';
|
||||
|
||||
void main() {
|
||||
// A container with the service's deps overridden but cancellationProvider left
|
||||
// alone, i.e. the root (main) isolate, where cancellationProvider has no
|
||||
// override and throws if read. The UI reads this provider here.
|
||||
ProviderContainer rootContainer() {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
localAlbumRepository.overrideWithValue(MockLocalAlbumRepository()),
|
||||
remoteAlbumRepository.overrideWithValue(MockRemoteAlbumRepository()),
|
||||
driftAlbumApiRepositoryProvider.overrideWithValue(MockDriftAlbumApiRepository()),
|
||||
storeServiceProvider.overrideWithValue(MockStoreService()),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Regression for #29125 (Sync Albums toggle) and #29119 (can't leave the album
|
||||
// selection screen): #28694 made the provider watch cancellationProvider, so
|
||||
// reading it off the isolate threw. The cancellation now lives on the isolate
|
||||
// call path, not the provider, so the UI can build it.
|
||||
test('builds on the root isolate without a cancellationProvider override', () {
|
||||
final container = rootContainer();
|
||||
|
||||
expect(() => container.read(syncLinkedAlbumServiceProvider), returnsNormally);
|
||||
expect(container.read(syncLinkedAlbumServiceProvider), isA<SyncLinkedAlbumService>());
|
||||
});
|
||||
|
||||
test('manageLinkedAlbums runs from the UI without a cancellation signal', () {
|
||||
final service = rootContainer().read(syncLinkedAlbumServiceProvider);
|
||||
|
||||
expect(service.manageLinkedAlbums(const [], 'user-1'), completes);
|
||||
});
|
||||
}
|
||||
@@ -16868,7 +16868,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"dateTimeRelative": {
|
||||
"description": "Relative time offset in minutes",
|
||||
"description": "Relative time offset in seconds",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"type": "integer"
|
||||
@@ -16965,7 +16965,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Client-side identifier echoed in the response to match results to inputs (e.g. filename)",
|
||||
"description": "Asset ID",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -18490,6 +18490,7 @@
|
||||
"properties": {
|
||||
"cronExpression": {
|
||||
"description": "Cron expression",
|
||||
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
@@ -25676,6 +25677,7 @@
|
||||
"properties": {
|
||||
"cronExpression": {
|
||||
"description": "Cron expression for when the integrity check should run",
|
||||
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
@@ -25708,6 +25710,7 @@
|
||||
"properties": {
|
||||
"cronExpression": {
|
||||
"description": "Cron expression for when the integrity check should run",
|
||||
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
@@ -25807,6 +25810,7 @@
|
||||
"properties": {
|
||||
"cronExpression": {
|
||||
"description": "Cron expression",
|
||||
"pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.12.4",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-sort-json": "^4.2.0",
|
||||
"semver": "^7.8.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.12.4",
|
||||
"@vitest/coverage-v8": "^4.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.12.4",
|
||||
"esbuild": "^0.28.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.12.4",
|
||||
"typescript": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,7 +673,7 @@ export type AssetMediaResponseDto = {
|
||||
export type AssetBulkUpdateDto = {
|
||||
/** Original date and time */
|
||||
dateTimeOriginal?: string;
|
||||
/** Relative time offset in minutes */
|
||||
/** Relative time offset in seconds */
|
||||
dateTimeRelative?: number;
|
||||
/** Asset description */
|
||||
description?: string;
|
||||
@@ -696,7 +696,7 @@ export type AssetBulkUpdateDto = {
|
||||
export type AssetBulkUploadCheckItem = {
|
||||
/** Base64 or hex encoded SHA1 hash */
|
||||
checksum: string;
|
||||
/** Client-side identifier echoed in the response to match results to inputs (e.g. filename) */
|
||||
/** Asset ID */
|
||||
id: string;
|
||||
};
|
||||
export type AssetBulkUploadCheckDto = {
|
||||
|
||||
Generated
+2690
-2767
File diff suppressed because it is too large
Load Diff
@@ -65,5 +65,3 @@ preferWorkspacePackages: true
|
||||
injectWorkspacePackages: true
|
||||
shamefullyHoist: false
|
||||
verifyDepsBeforeRun: install
|
||||
minimumReleaseAgeExclude:
|
||||
- '@immich/ui@0.81.1'
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606021219@sha256:63fa91aa011f6f2921dd32fe6d1be8d637e9bd7f3e3dd0c8e446afb31b282af4 AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202606161235@sha256:c6d59e3923f548d29a212b4dc51b6281a722cfa1da7972a009c0f3830f5762d6
|
||||
FROM ghcr.io/immich-app/base-server-prod:202606021219@sha256:6ef9ef5859492149af770a6c884b5e2ddbaeef99f8885ea5f2d9f73625a3d9ec
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606021219@sha256:63fa91aa011f6f2921dd32fe6d1be8d637e9bd7f3e3dd0c8e446afb31b282af4 AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.12.4",
|
||||
"@types/nodemailer": "^8.0.0",
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -58,7 +58,7 @@ const AssetMediaCreateSchema = AssetMediaBaseSchema.extend({
|
||||
|
||||
const AssetBulkUploadCheckItemSchema = z
|
||||
.object({
|
||||
id: z.string().describe('Client-side identifier echoed in the response to match results to inputs (e.g. filename)'),
|
||||
id: z.string().describe('Asset ID'),
|
||||
checksum: z.string().describe('Base64 or hex encoded SHA1 hash'),
|
||||
})
|
||||
.meta({ id: 'AssetBulkUploadCheckItem' });
|
||||
|
||||
@@ -41,7 +41,7 @@ const UpdateAssetBaseSchema = z
|
||||
const AssetBulkUpdateBaseSchema = UpdateAssetBaseSchema.extend({
|
||||
ids: z.array(z.uuidv4()).describe('Asset IDs to update'),
|
||||
duplicateId: z.string().nullish().describe('Duplicate ID'),
|
||||
dateTimeRelative: z.int().optional().describe('Relative time offset in minutes'),
|
||||
dateTimeRelative: z.int().optional().describe('Relative time offset in seconds'),
|
||||
timeZone: z.string().optional().describe('Time zone (IANA timezone)'),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { validateCronExpression } from 'cron';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import {
|
||||
@@ -44,16 +43,7 @@ const JobSettingsSchema = z
|
||||
|
||||
const cronExpressionSchema = z
|
||||
.string()
|
||||
.superRefine((value, ctx) => {
|
||||
const validated = validateCronExpression(value);
|
||||
if (!validated.valid) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `Invalid cron expression. ${validated.error?.message ?? ''}`,
|
||||
input: value,
|
||||
});
|
||||
}
|
||||
})
|
||||
.regex(/(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7}/, 'Invalid cron expression')
|
||||
.describe('Cron expression');
|
||||
|
||||
const DatabaseBackupSchema = z
|
||||
|
||||
@@ -129,10 +129,10 @@ from
|
||||
and "integrity_report"."type" = $1
|
||||
where
|
||||
"asset"."deletedAt" is null
|
||||
and "integrity_report"."createdAt" >= $2
|
||||
and "integrity_report"."createdAt" <= $3
|
||||
and "createdAt" >= $2
|
||||
and "createdAt" <= $3
|
||||
order by
|
||||
"integrity_report"."createdAt" asc
|
||||
"createdAt" asc
|
||||
|
||||
-- IntegrityRepository.streamIntegrityReports
|
||||
select
|
||||
|
||||
@@ -177,9 +177,9 @@ export class IntegrityRepository {
|
||||
'asset.id as assetId',
|
||||
'integrity_report.id as reportId',
|
||||
])
|
||||
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
|
||||
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
|
||||
.orderBy('integrity_report.createdAt', 'asc')
|
||||
.$if(startMarker !== undefined, (qb) => qb.where('createdAt', '>=', startMarker!))
|
||||
.$if(endMarker !== undefined, (qb) => qb.where('createdAt', '<=', endMarker!))
|
||||
.orderBy('createdAt', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
|
||||
@@ -369,26 +369,6 @@ describe(DuplicateService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarWrite, data: { id: asset1.id } }]);
|
||||
});
|
||||
|
||||
it('should not merge metadata when multiple assets are kept', async () => {
|
||||
const asset1 = AssetFactory.create({ isFavorite: true });
|
||||
const asset2 = AssetFactory.create();
|
||||
mocks.access.duplicate.checkOwnerAccess.mockResolvedValue(new Set(['group-1']));
|
||||
mocks.duplicateRepository.get.mockResolvedValue({
|
||||
duplicateId: 'group-1',
|
||||
assets: [asset1 as unknown as MapAsset, asset2 as unknown as MapAsset],
|
||||
});
|
||||
|
||||
const result = await sut.resolve(authStub.admin, {
|
||||
groups: [{ duplicateId: 'group-1', keepAssetIds: [asset1.id, asset2.id], trashAssetIds: [] }],
|
||||
});
|
||||
|
||||
expect(result[0].success).toBe(true);
|
||||
expect(mocks.album.addAssetIdsToAlbums).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.replaceAssetTags).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAllExif).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset1.id, asset2.id], { duplicateId: null });
|
||||
});
|
||||
|
||||
// NOTE: The following integration-style tests are covered by E2E tests instead
|
||||
// to avoid complex mock setup. The validation and error-handling logic above
|
||||
// is thoroughly unit tested.
|
||||
|
||||
@@ -156,51 +156,51 @@ export class DuplicateService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// Only merge metadata into the keeper when exactly one asset can absorb trashed duplicates.
|
||||
if (idsToKeep.length === 1 && idsToTrash.length > 0) {
|
||||
const assetAlbumMap = await this.albumRepository.getByAssetIds(auth.user.id, [...groupAssetIds]);
|
||||
const assetAlbumMap = await this.albumRepository.getByAssetIds(auth.user.id, [...groupAssetIds]);
|
||||
|
||||
const { assetUpdate, exifUpdate, mergedAlbumIds, mergedTagIds, mergedTagValues } = this.getSyncMergeResult(
|
||||
duplicateGroup.assets,
|
||||
assetAlbumMap,
|
||||
);
|
||||
const { assetUpdate, exifUpdate, mergedAlbumIds, mergedTagIds, mergedTagValues } = this.getSyncMergeResult(
|
||||
duplicateGroup.assets,
|
||||
assetAlbumMap,
|
||||
);
|
||||
|
||||
if (mergedAlbumIds.length > 0) {
|
||||
const allowedAlbumIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AlbumAssetCreate,
|
||||
ids: mergedAlbumIds,
|
||||
});
|
||||
if (mergedAlbumIds.length > 0) {
|
||||
const allowedAlbumIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AlbumAssetCreate,
|
||||
ids: mergedAlbumIds,
|
||||
});
|
||||
|
||||
const allowedShareIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AssetShare,
|
||||
ids: idsToKeep,
|
||||
});
|
||||
const allowedShareIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AssetShare,
|
||||
ids: idsToKeep,
|
||||
});
|
||||
|
||||
if (allowedAlbumIds.size > 0 && allowedShareIds.size > 0) {
|
||||
await this.albumRepository.addAssetIdsToAlbums(
|
||||
[...allowedAlbumIds].flatMap((albumId) => [...allowedShareIds].map((assetId) => ({ albumId, assetId }))),
|
||||
);
|
||||
}
|
||||
if (allowedAlbumIds.size > 0 && allowedShareIds.size > 0) {
|
||||
await this.albumRepository.addAssetIdsToAlbums(
|
||||
[...allowedAlbumIds].flatMap((albumId) => [...allowedShareIds].map((assetId) => ({ albumId, assetId }))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedTagIds.length > 0) {
|
||||
const allowedTagIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.TagAsset,
|
||||
ids: mergedTagIds,
|
||||
});
|
||||
if (mergedTagIds.length > 0) {
|
||||
const allowedTagIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.TagAsset,
|
||||
ids: mergedTagIds,
|
||||
});
|
||||
|
||||
if (allowedTagIds.size > 0) {
|
||||
await Promise.all(
|
||||
idsToKeep.map((assetId) => this.tagRepository.replaceAssetTags(assetId, [...allowedTagIds])),
|
||||
);
|
||||
if (allowedTagIds.size > 0) {
|
||||
// Replace tags for each keeper asset to ensure all merged tags are applied
|
||||
await Promise.all(idsToKeep.map((assetId) => this.tagRepository.replaceAssetTags(assetId, [...allowedTagIds])));
|
||||
|
||||
await this.assetRepository.updateAllExif(idsToKeep, { tags: mergedTagValues });
|
||||
}
|
||||
// Update asset_exif.tags so the subsequent SidecarWrite + MetadataExtraction
|
||||
// cycle preserves the merged tags (updateAllExif locks the property automatically)
|
||||
await this.assetRepository.updateAllExif(idsToKeep, { tags: mergedTagValues });
|
||||
}
|
||||
}
|
||||
|
||||
if (idsToKeep.length > 0) {
|
||||
const hasExifUpdate = Object.keys(exifUpdate).length > 0;
|
||||
const hasTagUpdate = mergedTagIds.length > 0;
|
||||
|
||||
@@ -213,8 +213,6 @@ export class DuplicateService extends BaseService {
|
||||
}
|
||||
|
||||
await this.assetRepository.updateAll(idsToKeep, { duplicateId: null, ...assetUpdate });
|
||||
} else if (idsToKeep.length > 0) {
|
||||
await this.assetRepository.updateAll(idsToKeep, { duplicateId: null });
|
||||
}
|
||||
|
||||
if (idsToTrash.length > 0) {
|
||||
|
||||
@@ -399,7 +399,10 @@ export class IntegrityService extends BaseService {
|
||||
await this.integrityRepository.deleteByIds(outdatedReports);
|
||||
}
|
||||
|
||||
const missingFiles = results.filter(({ exists }) => !exists);
|
||||
const missingFiles = Object.values(
|
||||
Object.fromEntries(results.filter(({ exists }) => !exists).map((file) => [file.path, file])),
|
||||
);
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
await this.integrityRepository.create(
|
||||
missingFiles.map(({ path, assetId, fileAssetId }) => ({
|
||||
|
||||
@@ -319,14 +319,14 @@ describe(SystemConfigService.name, () => {
|
||||
it('should accept valid cron expressions', async () => {
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(
|
||||
JSON.stringify({ library: { scan: { cronExpression: '0 0 */3 * *' } } }),
|
||||
JSON.stringify({ library: { scan: { cronExpression: '0 0 * * *' } } }),
|
||||
);
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toMatchObject({
|
||||
library: {
|
||||
scan: {
|
||||
enabled: true,
|
||||
cronExpression: '0 0 */3 * *',
|
||||
cronExpression: '0 0 * * *',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -512,6 +512,82 @@ describe(IntegrityService.name, () => {
|
||||
nextCursor: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fail when the same path is duplicated within a batch', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const storage = ctx.getMock(StorageRepository);
|
||||
|
||||
const {
|
||||
result: { id: ownerId },
|
||||
} = await ctx.newUser();
|
||||
|
||||
const {
|
||||
result: { id: assetId1 },
|
||||
} = await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate' });
|
||||
|
||||
const {
|
||||
result: { id: assetId2 },
|
||||
} = await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate' });
|
||||
|
||||
const fileAssetId1 = randomUUID();
|
||||
await ctx.newAssetFile({
|
||||
id: fileAssetId1,
|
||||
assetId: assetId1,
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/path/to/duplicate-file',
|
||||
});
|
||||
|
||||
const fileAssetId2 = randomUUID();
|
||||
await ctx.newAssetFile({
|
||||
id: fileAssetId2,
|
||||
assetId: assetId1,
|
||||
type: AssetFileType.Preview,
|
||||
path: '/path/to/duplicate-file',
|
||||
});
|
||||
|
||||
const {
|
||||
result: { id: assetId3 },
|
||||
} = await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate-cross' });
|
||||
|
||||
const fileAssetId3 = randomUUID();
|
||||
await ctx.newAssetFile({
|
||||
id: fileAssetId3,
|
||||
assetId: assetId3,
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/path/to/duplicate-cross',
|
||||
});
|
||||
|
||||
storage.stat.mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
await expect(
|
||||
sut.handleMissingFiles({
|
||||
items: [
|
||||
{ path: '/path/to/duplicate', assetId: assetId1, fileAssetId: null, reportId: null },
|
||||
{ path: '/path/to/duplicate', assetId: assetId2, fileAssetId: null, reportId: null },
|
||||
{ path: '/path/to/duplicate-file', assetId: null, fileAssetId: fileAssetId1, reportId: null },
|
||||
{ path: '/path/to/duplicate-file', assetId: null, fileAssetId: fileAssetId2, reportId: null },
|
||||
{ path: '/path/to/duplicate-cross', assetId: assetId3, fileAssetId: null, reportId: null },
|
||||
{ path: '/path/to/duplicate-cross', assetId: null, fileAssetId: fileAssetId3, reportId: null },
|
||||
],
|
||||
}),
|
||||
).resolves.toBe(JobStatus.Success);
|
||||
|
||||
await expect(
|
||||
ctx.get(IntegrityRepository).getIntegrityReport(
|
||||
{
|
||||
limit: 100,
|
||||
},
|
||||
IntegrityReport.MissingFile,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({ path: '/path/to/duplicate' }),
|
||||
expect.objectContaining({ path: '/path/to/duplicate-file' }),
|
||||
expect.objectContaining({ path: '/path/to/duplicate-cross' }),
|
||||
]),
|
||||
nextCursor: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleMissingRefresh', () => {
|
||||
@@ -686,6 +762,40 @@ describe(IntegrityService.name, () => {
|
||||
nextCursor: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fail when the same path is duplicated across assets', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const storage = ctx.getMock(StorageRepository);
|
||||
const job = ctx.getMock(JobRepository);
|
||||
job.queue.mockResolvedValue(void 0);
|
||||
|
||||
const {
|
||||
result: { id: ownerId },
|
||||
} = await ctx.newUser();
|
||||
|
||||
await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate', checksum: Buffer.from('mismatch-a') });
|
||||
await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate', checksum: Buffer.from('mismatch-b') });
|
||||
|
||||
storage.createPlainReadStream.mockImplementation(() => Readable.from('garbage data'));
|
||||
|
||||
await expect(sut.handleChecksumFiles({ refreshOnly: false })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
await expect(
|
||||
ctx.get(IntegrityRepository).getIntegrityReport(
|
||||
{
|
||||
limit: 100,
|
||||
},
|
||||
IntegrityReport.ChecksumFail,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
path: '/path/to/duplicate',
|
||||
}),
|
||||
],
|
||||
nextCursor: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleChecksumRefresh', () => {
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@immich/ui": "^0.81.1",
|
||||
"@immich/ui": "^0.80.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
icon?: string;
|
||||
title: string;
|
||||
valuePromise: MaybePromise<ValueData>;
|
||||
tooltip?: string;
|
||||
footer?: Snippet;
|
||||
}
|
||||
|
||||
let { icon, title, valuePromise, tooltip, footer }: Props = $props();
|
||||
let { icon, title, valuePromise, footer }: Props = $props();
|
||||
const zeros = (data?: ValueData) => {
|
||||
let length = 13;
|
||||
if (data) {
|
||||
@@ -33,7 +32,7 @@
|
||||
{#if icon}
|
||||
<Icon {icon} size="40" />
|
||||
{/if}
|
||||
<Text size="giant" fontWeight="medium" title={tooltip}>{title}</Text>
|
||||
<Text size="giant" fontWeight="medium">{title}</Text>
|
||||
</div>
|
||||
|
||||
{#await valuePromise}
|
||||
|
||||
@@ -208,13 +208,13 @@
|
||||
if (relativeDate) {
|
||||
const duration = Duration.fromISO(relativeDate);
|
||||
return {
|
||||
fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toUTC().toISO() : undefined,
|
||||
fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toISO() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fileCreatedAfter: dateAfter,
|
||||
fileCreatedBefore: dateBefore,
|
||||
fileCreatedAfter: dateAfter?.toUTC().toISO(),
|
||||
fileCreatedBefore: dateBefore?.toUTC().toISO(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
}
|
||||
|
||||
const handleSettingsClick = async () => {
|
||||
const settings = await modalManager.show(MapSettingsModal);
|
||||
const settings = await modalManager.show(MapSettingsModal, { settings: { ...$mapSettings } });
|
||||
if (settings) {
|
||||
const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
|
||||
$mapSettings = settings;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
rounded?: boolean | 'full';
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { rounded = true, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="inline-block h-min bg-primary px-3 py-1 text-center align-baseline text-xs leading-none whitespace-nowrap text-subtle"
|
||||
class:rounded-md={rounded === true}
|
||||
class:rounded-full={rounded === 'full'}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { mapSettings, type MapSettings } from '$lib/stores/preferences.store';
|
||||
import type { MapSettings } from '$lib/stores/preferences.store';
|
||||
import { Button, DatePicker, Field, FormModal, Select, Stack, Switch } from '@immich/ui';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { Duration } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
type Props = {
|
||||
settings: MapSettings;
|
||||
onClose: (settings?: MapSettings) => void;
|
||||
};
|
||||
|
||||
let { onClose }: Props = $props();
|
||||
let settings = $state({ ...$mapSettings });
|
||||
let { settings: initialValues, onClose }: Props = $props();
|
||||
let settings = $state(initialValues);
|
||||
|
||||
let customDateRange = $state(!!settings.dateAfter || !!settings.dateBefore);
|
||||
|
||||
@@ -40,17 +41,10 @@
|
||||
{#if customDateRange}
|
||||
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
|
||||
<Field label={$t('date_after')}>
|
||||
<DatePicker
|
||||
value={DateTime.fromISO(settings.dateAfter ?? '')}
|
||||
maxDate={DateTime.fromISO(settings.dateBefore ?? '')}
|
||||
onChange={(date) => (settings.dateAfter = date?.toUTC().toISO() ?? undefined)}
|
||||
/>
|
||||
<DatePicker bind:value={settings.dateAfter} maxDate={settings.dateBefore} />
|
||||
</Field>
|
||||
<Field label={$t('date_before')}>
|
||||
<DatePicker
|
||||
value={DateTime.fromISO(settings.dateBefore ?? '')}
|
||||
onChange={(date) => (settings.dateBefore = date?.toUTC().toISO() ?? undefined)}
|
||||
/>
|
||||
<DatePicker bind:value={settings.dateBefore} />
|
||||
</Field>
|
||||
<div class="flex justify-center">
|
||||
<Button
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { MediaType, QueryType, validQueryTypes } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { SearchFilter } from '$lib/types';
|
||||
import { asLocalTimeISO, parseUtcDate } from '$lib/utils/date-time';
|
||||
import { parseUtcDate } from '$lib/utils/date-time';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
|
||||
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
let { searchQuery, onClose }: Props = $props();
|
||||
|
||||
const parseOptionalDate = (dateString?: DateTime) => (dateString ? parseUtcDate(dateString.toString()) : undefined);
|
||||
const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day') || undefined;
|
||||
const formId = generateId();
|
||||
|
||||
@@ -143,12 +144,8 @@
|
||||
make: filter.camera.make,
|
||||
model: filter.camera.model,
|
||||
lensModel: filter.camera.lensModel,
|
||||
takenAfter: filter.date.takenAfter
|
||||
? asLocalTimeISO(filter.date.takenAfter.startOf('day') as DateTime<true>)
|
||||
: undefined,
|
||||
takenBefore: filter.date.takenBefore
|
||||
? asLocalTimeISO(filter.date.takenBefore.endOf('day') as DateTime<true>)
|
||||
: undefined,
|
||||
takenAfter: parseOptionalDate(filter.date.takenAfter)?.startOf('day').toISO() || undefined,
|
||||
takenBefore: parseOptionalDate(filter.date.takenBefore)?.endOf('day').toISO() || undefined,
|
||||
visibility: filter.display.isArchive ? AssetVisibility.Archive : undefined,
|
||||
isFavorite: filter.display.isFavorite || undefined,
|
||||
isNotInAlbum: filter.display.isNotInAlbum || undefined,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DateTime } from 'luxon';
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
import { browser } from '$app/environment';
|
||||
import { defaultLang } from '$lib/constants';
|
||||
@@ -26,8 +27,8 @@ export interface MapSettings {
|
||||
withPartners: boolean;
|
||||
withSharedAlbums: boolean;
|
||||
relativeDate: string;
|
||||
dateAfter?: string;
|
||||
dateBefore?: string;
|
||||
dateAfter?: DateTime<true>;
|
||||
dateBefore?: DateTime<true>;
|
||||
}
|
||||
|
||||
const defaultMapSettings = {
|
||||
|
||||
@@ -116,7 +116,6 @@ const nonIntlNames: Record<string, string> = {
|
||||
kxm: 'Khmer Surin',
|
||||
mfa: 'Malay (Pattani)',
|
||||
swg: 'Schwäbisch',
|
||||
tl: 'Tagalog',
|
||||
};
|
||||
|
||||
const getLanguageName = (code: string) =>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { t, type Translations } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
type Props = {
|
||||
data: PageData;
|
||||
@@ -51,7 +50,7 @@
|
||||
};
|
||||
|
||||
let jobs: QueuesResponseLegacyDto | undefined = $state();
|
||||
const activeJobs = new SvelteSet<ManualJobName>();
|
||||
let expectingUpdate: boolean = $state(false);
|
||||
|
||||
const getReportTypeTranslation = (report: IntegrityReport): Translations => {
|
||||
switch (report) {
|
||||
@@ -67,27 +66,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
const getReportTypeDescriptionKey = (report: IntegrityReport): Translations => {
|
||||
switch (report) {
|
||||
case IntegrityReport.UntrackedFile: {
|
||||
return 'admin.maintenance_integrity_untracked_file_description';
|
||||
}
|
||||
case IntegrityReport.MissingFile: {
|
||||
return 'admin.maintenance_integrity_missing_file_description';
|
||||
}
|
||||
case IntegrityReport.ChecksumMismatch: {
|
||||
return 'admin.maintenance_integrity_checksum_mismatch_description';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateReports = async () => {
|
||||
jobs = await getQueuesLegacy();
|
||||
if (jobs.integrityCheck.queueStatus.isActive) {
|
||||
activeJobs.add(ManualJobName.IntegrityUntrackedFilesRefresh);
|
||||
} else if (activeJobs.size > 0) {
|
||||
activeJobs.clear();
|
||||
expectingUpdate = true;
|
||||
} else if (expectingUpdate) {
|
||||
integrityReport = await getIntegrityReportSummary();
|
||||
expectingUpdate = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +84,7 @@
|
||||
|
||||
const onJobCreate = ({ dto }: { dto: JobCreateDto }) => {
|
||||
if ((Object.values(jobNames).includes(dto.name) || Object.values(refreshJobNames).includes(dto.name)) && jobs) {
|
||||
activeJobs.add(dto.name);
|
||||
expectingUpdate = true;
|
||||
jobs.integrityCheck.queueStatus.isActive = true;
|
||||
}
|
||||
};
|
||||
@@ -121,7 +106,7 @@
|
||||
}
|
||||
}}
|
||||
class="mt-1 self-end"
|
||||
disabled={activeJobs.size > 0}>{$t('admin.maintenance_integrity_check_all')}</Button
|
||||
disabled={expectingUpdate}>{$t('admin.maintenance_integrity_check_all')}</Button
|
||||
>
|
||||
<Button
|
||||
size="tiny"
|
||||
@@ -132,7 +117,7 @@
|
||||
}
|
||||
}}
|
||||
class="mt-1 self-end"
|
||||
disabled={activeJobs.size > 0}>{$t('refresh')}</Button
|
||||
disabled={expectingUpdate}>{$t('refresh')}</Button
|
||||
></HStack
|
||||
>
|
||||
|
||||
@@ -140,36 +125,36 @@
|
||||
{#each reportTypes as reportType (reportType)}
|
||||
<ServerStatisticsCard
|
||||
title={$t(getReportTypeTranslation(reportType))}
|
||||
tooltip={$t(getReportTypeDescriptionKey(reportType))}
|
||||
valuePromise={{ value: integrityReport[reportType] }}
|
||||
>
|
||||
{#snippet footer()}
|
||||
<HStack gap={1} class="justify-between">
|
||||
<HStack gap={0}>
|
||||
<Button
|
||||
onclick={() =>
|
||||
handleCreateJob({
|
||||
name: jobNames[reportType],
|
||||
})}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
disabled={activeJobs.has(jobNames[reportType])}>{$t('admin.maintenance_integrity_check')}</Button
|
||||
>
|
||||
<Button
|
||||
onclick={() =>
|
||||
handleCreateJob({
|
||||
name: refreshJobNames[reportType],
|
||||
})}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
disabled={activeJobs.has(refreshJobNames[reportType])}>{$t('refresh')}</Button
|
||||
>
|
||||
</HStack>
|
||||
<HStack gap={1} class="justify-end">
|
||||
<Button
|
||||
onclick={() =>
|
||||
handleCreateJob({
|
||||
name: jobNames[reportType],
|
||||
})}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
class="mt-1 self-end"
|
||||
disabled={expectingUpdate}>{$t('admin.maintenance_integrity_check_all')}</Button
|
||||
>
|
||||
<Button
|
||||
onclick={() =>
|
||||
handleCreateJob({
|
||||
name: refreshJobNames[reportType],
|
||||
})}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
class="mt-1 self-end"
|
||||
disabled={expectingUpdate}>{$t('refresh')}</Button
|
||||
>
|
||||
<Button
|
||||
href={`${Route.systemMaintenanceIntegrityReport({
|
||||
reportType,
|
||||
})}`}
|
||||
size="tiny">{$t('view')}</Button
|
||||
size="tiny"
|
||||
class="mt-1 self-end">{$t('view')}</Button
|
||||
>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
@@ -181,7 +166,7 @@
|
||||
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
||||
<Text size="small">{$t('admin.maintenance_backup_management')}</Text>
|
||||
<Text size="small">{$t('admin.maintenance_settings')}</Text>
|
||||
|
||||
<SettingAccordion
|
||||
title={$t('admin.maintenance_restore_database_backup')}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import { cleanClass } from '$lib';
|
||||
import QueueCardBadge from './QueueCardBadge.svelte';
|
||||
import QueueCardButton from './QueueCardButton.svelte';
|
||||
import Badge from '$lib/elements/Badge.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { asQueueItem } from '$lib/services/queue.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { transformToTitleCase } from '$lib/utils';
|
||||
import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk';
|
||||
import { Badge, Icon, IconButton, Link } from '@immich/ui';
|
||||
import { Icon, IconButton, Link } from '@immich/ui';
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiAllInclusive,
|
||||
@@ -66,16 +67,27 @@
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
{#if statistics.failed > 0}
|
||||
<Badge
|
||||
onClose={() => onCommand({ command: QueueCommand.ClearFailed, force: false })}
|
||||
translations={{ close: $t('clear_message') }}
|
||||
>
|
||||
{$t('admin.jobs_failed', { values: { jobCount: statistics.failed.toLocaleString($locale) } })}
|
||||
<Badge>
|
||||
<div class="flex flex-row gap-1">
|
||||
<span class="text-sm">
|
||||
{$t('admin.jobs_failed', { values: { jobCount: statistics.failed.toLocaleString($locale) } })}
|
||||
</span>
|
||||
<IconButton
|
||||
color="primary"
|
||||
icon={mdiClose}
|
||||
aria-label={$t('clear_message')}
|
||||
size="tiny"
|
||||
shape="round"
|
||||
onclick={() => onCommand({ command: QueueCommand.ClearFailed, force: false })}
|
||||
/>
|
||||
</div>
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if statistics.delayed > 0}
|
||||
<Badge>
|
||||
{$t('admin.jobs_delayed', { values: { jobCount: statistics.delayed.toLocaleString($locale) } })}
|
||||
<span class="text-sm">
|
||||
{$t('admin.jobs_delayed', { values: { jobCount: statistics.delayed.toLocaleString($locale) } })}
|
||||
</span>
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user