mirror of
https://github.com/immich-app/immich.git
synced 2026-06-22 14:52:17 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7542f204d |
+1
-1
@@ -10,7 +10,7 @@ DB_DATA_LOCATION=./postgres
|
||||
# TZ=Etc/UTC
|
||||
|
||||
# The Immich version to use. You can pin this to a specific version like "v2.1.0"
|
||||
IMMICH_VERSION=v3
|
||||
IMMICH_VERSION=v2
|
||||
|
||||
# Connection secret for postgres. You should change it to a random password
|
||||
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||
|
||||
@@ -19,7 +19,7 @@ If this does not work, try running `docker compose up -d --force-recreate`.
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
| :----------------- | :------------------------------ | :-----: | :----------------------- |
|
||||
| `IMMICH_VERSION` | Image tags | `v3` | server, machine learning |
|
||||
| `IMMICH_VERSION` | Image tags | `v2` | server, machine learning |
|
||||
| `UPLOAD_LOCATION` | Host path for uploads | | server |
|
||||
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ docker image prune
|
||||
## Versioning Policy
|
||||
|
||||
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
|
||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`.
|
||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v2`.
|
||||
|
||||
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
|
||||
Switching back to an earlier version, even within the same minor release tag, is not supported.
|
||||
|
||||
+1
-1
@@ -1548,7 +1548,7 @@
|
||||
"map_location_picker_page_use_location": "Use this location",
|
||||
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
|
||||
"map_location_service_disabled_title": "Location Service disabled",
|
||||
"map_marker_for_image": "Map marker for image taken in {city}, {country}",
|
||||
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
|
||||
"map_marker_with_image": "Map marker with image",
|
||||
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||
"map_no_location_permission_title": "Location Permission denied",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:20ec607c68642c64c73269ce245aa0f060913f4129d15d9687850aa158e6269c AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:6fe9bb8b5e309a4fc598acb9d3ad18718f7519911eb6af9a1d9121d960f0ed75 AS builder-cpu
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS builder-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:c33f0bc4364a6881bed1ec0cc2665e6c53c87a43e774aaeab88e6f17af105e4f 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:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS prod-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:c33f0bc4364a6881bed1ec0cc2665e6c53c87a43e774aaeab88e6f17af105e4f AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
|
||||
Generated
+55
-55
@@ -554,7 +554,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.136.3"
|
||||
version = "0.138.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -563,9 +563,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/58/ff455d9fe47c60abadb34b9e05a304b1f05f5ab8000ac01565156b6f5e43/fastapi-0.138.0.tar.gz", hash = "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061", size = 419240, upload-time = "2026-06-20T01:18:05.259Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1309,7 +1309,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.44.1"
|
||||
version = "2.44.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "configargparse" },
|
||||
@@ -1329,9 +1329,9 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/98/1501e70415157ec47c9020a682a0119c8ead46ae9f1aa79e007f779fda14/locust-2.44.1.tar.gz", hash = "sha256:962e6498431f152eca26d9cb158c23e3f61bf26e026f4f4171bed3f713379820", size = 1437151, upload-time = "2026-06-01T22:41:58.604Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/58/a9642fa8604ae13d87b408c8c90cac99f3cfd59af59a085ac7435e825f68/locust-2.44.4.tar.gz", hash = "sha256:9115530e33ca9591c02d9f970d92991739d50b775785b954103d5794ce299c3f", size = 1437324, upload-time = "2026-06-18T15:35:55.133Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/f9/45b4c7849051deb080ba90c86bbe7d96969ba8858894eebcfdd921a173ac/locust-2.44.1-py3-none-any.whl", hash = "sha256:dba774c609fd56402ddf1ca07e3040ae7b436b4c2027393288e83167d24ac397", size = 1458058, upload-time = "2026-06-01T22:41:57.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/33/5d3ba61b58a48e4eb4bbf6b3237d9dcba37b53bc0fa36a5f4e98947f3c67/locust-2.44.4-py3-none-any.whl", hash = "sha256:3a600f2162e79d47f2bf2bf29cd7d4c066684a89f814b33d6fa68147a89a2b02", size = 1458356, upload-time = "2026-06-18T15:35:53.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1746,7 +1746,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime"
|
||||
version = "1.26.0"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flatbuffers" },
|
||||
@@ -1755,30 +1755,30 @@ dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/81/29a9eb470994a75eb7b3ccf32be314d7c66675a00ac7b50294816cc2db27/onnxruntime-1.26.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ee1109ef4ef27cad90e823399e61e03b3c6c7bfe0fb820b4baf3678c15be8b3c", size = 18005108, upload-time = "2026-05-08T19:08:11.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/c7/73efa6c8a4000c38fcc14947d84f234a17e5d66f203b37b7f1ad4a7b46eb/onnxruntime-1.26.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35c7c7b0ac2e02001d28fab6c9fc24e9abc5e6faa35e6e19c63cecf1406ba89f", size = 16043752, upload-time = "2026-05-08T19:07:10.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/3f/8de630f595daf6ce884d4dd95afd2a60e70ec6572e52bfee3aa2229befab/onnxruntime-1.26.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11a8df4dcfe9ad5ff0bd71a7571dbed019fabc7594676c89fe8b86ea029c246f", size = 18176043, upload-time = "2026-05-08T19:07:33.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/21/9f041de20787cd85498bd48e0ec4d098bf2a6c486e25b24b8dae1bf492b2/onnxruntime-1.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:e6456718125fd777c673f3b78d4a9ab58d6adea641e9afae85ee6444f0e0e9a9", size = 13023165, upload-time = "2026-05-08T19:08:00.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/82/3b9fe0ead2557cc3adf74c74c141bd1c7c4c6a9548c610af37df199f4512/onnxruntime-1.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:cd920e45b730e4a87833e2910d8ca375aaca9da6ccc09e24bce463b3356d637f", size = 12789514, upload-time = "2026-05-08T19:07:49.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b1/d111b1df656761f980d9e298a60039a9cb66036b1d039e777537743d0ac3/onnxruntime-1.26.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05b028781b322ad74b57ce5b50aa5280bb1fe96ceec334628ade681e0b24c1ac", size = 18016624, upload-time = "2026-05-12T00:41:01.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/a0/3f9d896a0385a36bd04345d6d0b802821a5782adde562e7e135f6bb71c73/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0", size = 16052692, upload-time = "2026-05-08T19:07:13.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/43/2a4e04f8dbeffad19bbcced4bcd4289bf478921518437404d6b92bdf213b/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6", size = 18185439, upload-time = "2026-05-08T19:07:36.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/fc/026d0a7162b9c2153dac292baea9e027c42304dc1d9dc6f8ff5b4cfbaedd/onnxruntime-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:a26374dc7fbcaae593601086b242120e13f2310558df0991da6dd8b8fac00414", size = 13026427, upload-time = "2026-05-08T19:08:03.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/27/1dcf88e45e4c69db5f7b106f2dacc3801ba98994e082ca03e1dfdf7bfe57/onnxruntime-1.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:54a8053410fd31fd66469bd754fcfe8a4df9f7eb44756b4b5479bf50c842d948", size = 12796647, upload-time = "2026-05-08T19:07:52.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/89/3e52249aa08fa301e217ecba07b5246a8338fa2b401e109326e3fc5be0f9/onnxruntime-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:61bec80655efa460591c2bc655392d57d2650ce85533a6b9b3b7a790d7ea7916", size = 13026751, upload-time = "2026-05-08T19:08:06.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b3/c1c8782b14af6797c303de132d6eef26a9fb80dfacd3750ce57911d11c6b/onnxruntime-1.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a6677545ff451e3539a02746d2f207d8c5baa4a0a818886bb9d6a6eb9511ee89", size = 12796807, upload-time = "2026-05-08T19:07:54.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/89/17546c1c20f6bfc3ae41c22152378a26edfea918af3129e2139dcd7c99f3/onnxruntime-1.26.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:33a791f31432a3af1a96db5e54818b37aba5e5eefc2e6af5794c10a9118a9993", size = 18019724, upload-time = "2026-05-08T19:07:30.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/24/89457a35f6af29538a76647f2c18c3a28277e6c19234c847e7b4b7c19860/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e90c00732c4553618103149d93f688e8c3063017938f8983e21a71d9f3b6d22e", size = 16054821, upload-time = "2026-05-08T19:07:22.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/f9/15b2e1815cf570d238e0135529f80d2dce64e8e8818a1489cae83823c5c6/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01498e80ba8988428d08c2d51b1338f89e3de2a93e6ffe555f79c68f26a5c06b", size = 18185815, upload-time = "2026-05-08T19:07:44.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/65/2e11055faf015e4b07f45b513fa49b391baf2e19d92d77d73ebee13c1004/onnxruntime-1.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:7ead61450d8405167c87dd3a31d8da1d576b490a57dab1aa8b82a7da6825f5aa", size = 13349887, upload-time = "2026-05-08T19:08:08.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e4/0f9d1a5718b1781c610c1e354765a3820597081754277a6a9a2b50705702/onnxruntime-1.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:31d71a53490e46910877d0902b5ad99c69a5955e5c7ea6c82863519410e1ba7c", size = 13140121, upload-time = "2026-05-08T19:07:57.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/42/3b8e635f067d06d9f45bede470b8d539d101a4166c272213158dfd08b6ce/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b6d258fb78fdfcf049795bcfaa74dcb90ae7baa277afd21e6fd28b83f2c496", size = 16057240, upload-time = "2026-05-08T19:07:25.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/99/f2be40a31b908d96b861ae0ce98582fa376c18a7f816b9d5eb4cd6aa0a4c/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4eefd386a45202aefb7a5132b94f32df9d506c9edcc7faf2fc60d65183f4b183", size = 18197382, upload-time = "2026-05-08T19:07:46.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/e4/5353d7e09ced4a8f473f843223fc75d726b2b5519dcefc12f22a6c92852d/onnxruntime-1.27.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8ba14a38c570087f3cdb8cfba33f7a38a1e826c1e5b29e17c28ceda0cc910016", size = 18416484, upload-time = "2026-06-15T22:43:43.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1f/a2117aa3f144fce88774efa37440d0ca72d0c9144854dfc0961f2b04c6fc/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eb083321af8a236a84c7c140a7f4cecbfa2a987a18c07c78db471c20cd390ef", size = 16419330, upload-time = "2026-06-15T22:42:37.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/cd/74bb804170ceb622fda9111df31a07b3024f7491472256d3a90b5391a4d2/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4f7b0e90d2d212e2c2deaa6c8291616183ab815d3ec558ea12d3ac8b26d36f4", size = 18636930, upload-time = "2026-06-15T22:43:01.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/8f/5b8e2b85e81735696887175dbaf6409f215683f5ca9d4928fbb038211d32/onnxruntime-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:ff050e4f6bf7f12918fa14dcb047c0b02e295f35e86d42532552be4b3d54e977", size = 13356110, upload-time = "2026-06-15T22:43:32.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3a/4f568de678126b6a371a93862f015a82138359decd97fcac61fc84b5b774/onnxruntime-1.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:75fbc1e1fb43a39a856c8209c544cca7817b5de7ac16b15b1bdf55d1cc67b9df", size = 13098635, upload-time = "2026-06-15T22:43:19.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/b7/dd3a524ed93a820dff1af902d0412957ab12499953333e9daa01af5bc480/onnxruntime-1.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a14c2ce45312def86b77aea651f46565e45960cf5f0721bfdff449165086ab76", size = 18433506, upload-time = "2026-06-15T22:43:47.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/86/c3b6b17745a1997d784dadc9bd88d713d2e6721139a5a0e885b28cfb79b1/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fddce0539a4898c7bef35b052ffd37935b2190e35488eab99ce91887743ea1", size = 16438140, upload-time = "2026-06-15T22:42:40.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/81/24dd9b31b0fb912ee19ca53ac1c9764bfd79d58a2ccef564eb693be831a5/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c65a7438632d55dfbc8a02ee60bd6cf7dd9d1ba05a43d4b851452f32338e194", size = 18658316, upload-time = "2026-06-15T22:43:04.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/88/8ec9db1a4d126bb8b758992beb40d1249df171917d75f44a327eb5f20dda/onnxruntime-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:20c321cf187ba496e648acf6b4cf90b4d398b0d17c2a77fdaeba365b908cc1c1", size = 13358769, upload-time = "2026-06-15T22:43:34.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/9f/fdad359dfcba7e7cd8815569b304a596531d4efa77a75d77f8b4981891a2/onnxruntime-1.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:d0d1f68868e2ef30ef70998ba9bbbc5c305e9b17041e3936751c1b8aa6aade06", size = 13104440, upload-time = "2026-06-15T22:43:22.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/2b/54208fd03ad410480bc17edf4869376362da8bbf46fe186ddf4cb5cc20fe/onnxruntime-1.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:b3e5b58b8c89c2b20e086e890aa9527377e5c240dc3ecc1640d18e07705eeb1c", size = 18432958, upload-time = "2026-06-15T22:42:53.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/24fc51fcbb126da6d032372314e47b55c3faad58f2aa78c0e199ccd20b9c/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b3d87eb560ff6a772240506f3c78d6d27c63cafedd5c775672e1194f968cfd", size = 16438180, upload-time = "2026-06-15T22:42:43.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/19/14929c3c2fe0b79b41cce24463062bf3afa4cdd3c19dccf00319caa92bff/onnxruntime-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6872443f236a554921cda6f318c900e2d0c226792cf3534d00e5057c6926e5d2", size = 18658445, upload-time = "2026-06-15T22:43:08.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/76/59ed932b0244acd7bbbd6449480053a6d958ea66357f022f932872e19287/onnxruntime-1.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:760021bca514d64a811837820d351a08a41741f16f8b4c26450da708fecf14e6", size = 13357856, upload-time = "2026-06-15T22:43:37.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/51/d1ec60ec7b1e2ae2d7340ba52b8a13529140039cd4407ba8dddbbc046582/onnxruntime-1.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:2fdfa9df40a0ded0028ce6f9cd863264237f3970559dea2b81456e9ac4622b94", size = 13104412, upload-time = "2026-06-15T22:43:27.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/7d/e6bb1c6445c94f708c38cd8fbb7bf0264108c33498b9445c93e60fe6d329/onnxruntime-1.27.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54c0c4e9202c36c4ecdb1f3443f5dfbfd5ee3b54d1362c4b4c6134110e74fb32", size = 16443331, upload-time = "2026-06-15T22:42:45.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/1b/b18b31e806eabc41077810199fbbb36fbc2d5f19912416e5ccfbf73053d1/onnxruntime-1.27.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b215aa662c8f983f7d6dedafe65a9be72c26e5338e0fe98b3e0422c32c85428", size = 18670967, upload-time = "2026-06-15T22:43:10.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/37/48ab79c39b58a7c9f6f5aac1fa0ff2b993eb2643393d6ed9e839ddb6f347/onnxruntime-1.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0874edc171f470fc4dd2bbb60bc0989612ed1a8b89b365cda016630a93227f13", size = 18433941, upload-time = "2026-06-15T22:42:58.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/24/d535ca8a09dbf697f853377c8dc0820dbcaae5f334316b400b953afbcba8/onnxruntime-1.27.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b51c014cf1a4fcd93c29a97eac8071fa27710dae05a4d0380bb60a66d60a62c", size = 16439970, upload-time = "2026-06-15T22:42:48.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b1/ea9ee80c0bdaa4efb13f29f8c236f3740f6655e8c092a2d119515a5a652c/onnxruntime-1.27.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:445fb702ea5241ba813a3ce2febe2e9408a64f6ad2eb610924322c536165f7cd", size = 18659240, upload-time = "2026-06-15T22:43:13.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/f2/1404507d76a21940e8bf46f414e3d1abd94dc888cb89a30f4a540275846f/onnxruntime-1.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:49e416be0d717338b6d041b99911b716d70c397d277056450724f93bdded3fc2", size = 13685306, upload-time = "2026-06-15T22:43:40.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e5/ca5cf012ccccb806c70e94aadfebca5606acc62b33eb88cec13352d0778f/onnxruntime-1.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:856032937dd3bc7a7c141909c8d7ae4fde3e3f59bddf061ae627b9a051bda95c", size = 13456280, upload-time = "2026-06-15T22:43:29.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7b/dca330a8397e9d816c976d7aed4e24a4a2d279bb1e551e3d0221d1389b1d/onnxruntime-1.27.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6197a02e3f620c4dc13cff51b80672409fc1ffab3aa2593911b19fd322ff48b", size = 16443274, upload-time = "2026-06-15T22:42:50.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/f6/2bac21f722aa45d876d4a51f26bd0ef30e704068a3cd5021a5a7cd784271/onnxruntime-1.27.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:370d211e1ceeac4cd5f45301655463ac59e27cdc74d9f7aeb2d19ff4b7a76715", size = 18670781, upload-time = "2026-06-15T22:43:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2285,7 +2285,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -2294,9 +2294,9 @@ dependencies = [
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2528,7 +2528,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "rapidocr"
|
||||
version = "3.8.1"
|
||||
version = "3.8.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorlog" },
|
||||
@@ -2544,7 +2544,7 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2638,27 +2638,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.16"
|
||||
version = "0.15.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -263,7 +263,6 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
child: MaterialApp.router(
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: true,
|
||||
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
|
||||
String? getVersionCompatibilityMessage(SemVer serverVersion, SemVer appVersion) {
|
||||
String? getVersionCompatibilityMessage(int _, int appMinor, int _, int serverMinor) {
|
||||
// Add latest compat info up top
|
||||
|
||||
// ensure mobile app major version is not behind server major version
|
||||
if (appVersion.major < serverVersion.major) {
|
||||
return 'Your mobile app version is not compatible with the server! Please update your mobile app to the latest version.';
|
||||
}
|
||||
|
||||
// ensure mobile app major version is not ahead of server major version by more than 1 major version
|
||||
if (appVersion.major > serverVersion.major + 1) {
|
||||
return 'Your server version is not compatible with the mobile app! Please update your server to the latest version.';
|
||||
if (serverMinor < 106 && appMinor >= 106) {
|
||||
return 'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login';
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -26,7 +26,6 @@ import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/provider_utils.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||
@@ -89,9 +88,18 @@ class LoginForm extends HookConsumerWidget {
|
||||
checkVersionMismatch() async {
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final appSemVer = SemVer.fromString(packageInfo.version);
|
||||
final serverSemVer = serverInfo.serverVersion;
|
||||
warningMessage.value = getVersionCompatibilityMessage(appSemVer, serverSemVer);
|
||||
final appVersion = packageInfo.version;
|
||||
final appMajorVersion = int.parse(appVersion.split('.')[0]);
|
||||
final appMinorVersion = int.parse(appVersion.split('.')[1]);
|
||||
final serverMajorVersion = serverInfo.serverVersion.major;
|
||||
final serverMinorVersion = serverInfo.serverVersion.minor;
|
||||
|
||||
warningMessage.value = getVersionCompatibilityMessage(
|
||||
appMajorVersion,
|
||||
appMinorVersion,
|
||||
serverMajorVersion,
|
||||
serverMinorVersion,
|
||||
);
|
||||
} catch (error) {
|
||||
warningMessage.value = 'Error checking version compatibility';
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export 'src/components/text_button.dart';
|
||||
export 'src/components/text_input.dart';
|
||||
export 'src/components/url_input.dart';
|
||||
export 'src/constants.dart';
|
||||
export 'src/snackbar.dart';
|
||||
export 'src/theme.dart';
|
||||
export 'src/translation.dart';
|
||||
export 'src/types.dart';
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ImmichColorOverride extends InheritedWidget {
|
||||
const ImmichColorOverride({super.key, required this.color, required super.child});
|
||||
|
||||
final Color color;
|
||||
|
||||
static Color? maybeOf(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<ImmichColorOverride>()?.color;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ImmichColorOverride oldWidget) => color != oldWidget.color;
|
||||
}
|
||||
@@ -16,9 +16,10 @@ class ImmichCloseButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ImmichIconButton(
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onPressed: onPressed ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
key: key,
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onPressed: onPressed ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class _ImmichFormState extends State<ImmichForm> {
|
||||
builder: (context, _) => ImmichTextButton(
|
||||
labelText: submitText,
|
||||
icon: widget.submitIcon,
|
||||
variant: .filled,
|
||||
variant: ImmichVariant.filled,
|
||||
loading: _controller.isLoading,
|
||||
onPressed: _controller.submit,
|
||||
disabled: _controller.onSubmit == null,
|
||||
|
||||
@@ -94,12 +94,12 @@ class _ImmichFormattedTextState extends State<ImmichFormattedText> {
|
||||
|
||||
final tag = match.group(1)!.toLowerCase();
|
||||
final content = match.group(2)!;
|
||||
final span = widget.spanBuilder?.call(tag);
|
||||
final style = span?.style ?? _defaultTextStyle(tag);
|
||||
final formattedSpan = (widget.spanBuilder ?? _defaultSpanBuilder)(tag);
|
||||
final style = formattedSpan.style ?? _defaultTextStyle(tag);
|
||||
|
||||
GestureRecognizer? recognizer;
|
||||
if (span?.onTap != null) {
|
||||
recognizer = TapGestureRecognizer()..onTap = span!.onTap;
|
||||
if (formattedSpan.onTap != null) {
|
||||
recognizer = TapGestureRecognizer()..onTap = formattedSpan.onTap;
|
||||
_recognizers.add(recognizer);
|
||||
}
|
||||
spans.add(TextSpan(text: content, style: style, recognizer: recognizer));
|
||||
@@ -114,12 +114,19 @@ class _ImmichFormattedTextState extends State<ImmichFormattedText> {
|
||||
return spans;
|
||||
}
|
||||
|
||||
FormattedSpan _defaultSpanBuilder(String tag) => switch (tag) {
|
||||
'b' => const FormattedSpan(style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
'link' => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)),
|
||||
_ when tag.endsWith('-link') => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)),
|
||||
_ => const FormattedSpan(),
|
||||
};
|
||||
|
||||
TextStyle? _defaultTextStyle(String tag) => switch (tag) {
|
||||
'b' => const TextStyle(fontWeight: FontWeight.bold),
|
||||
'link' => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ when tag.endsWith('-link') => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ => null,
|
||||
};
|
||||
'b' => const TextStyle(fontWeight: FontWeight.bold),
|
||||
'link' => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ when tag.endsWith('-link') => const TextStyle(decoration: TextDecoration.underline),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,80 +1,54 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichIconButton extends StatefulWidget {
|
||||
class ImmichIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final VoidCallback onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool disabled;
|
||||
final bool? loading;
|
||||
|
||||
const ImmichIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.color = .primary,
|
||||
this.variant = .filled,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.disabled = false,
|
||||
this.loading,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichIconButton> createState() => _ImmichIconButtonState();
|
||||
}
|
||||
|
||||
class _ImmichIconButtonState extends State<ImmichIconButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final background = switch (widget.variant) {
|
||||
.filled => switch (widget.color) {
|
||||
.primary => colorScheme.primary,
|
||||
.secondary => colorScheme.secondary,
|
||||
},
|
||||
.ghost => Colors.transparent,
|
||||
final background = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
ImmichVariant.ghost => Colors.transparent,
|
||||
};
|
||||
|
||||
final foreground =
|
||||
context.colorOverride ??
|
||||
switch (widget.variant) {
|
||||
.filled => switch (widget.color) {
|
||||
.primary => colorScheme.onPrimary,
|
||||
.secondary => colorScheme.onSecondary,
|
||||
},
|
||||
.ghost => switch (widget.color) {
|
||||
.primary => colorScheme.primary,
|
||||
.secondary => colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
final foreground = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => colorScheme.onPrimary,
|
||||
ImmichColor.secondary => colorScheme.onSecondary,
|
||||
},
|
||||
ImmichVariant.ghost => switch (color) {
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
final effectiveOnPressed = disabled ? null : onPressed;
|
||||
|
||||
return IconButton(
|
||||
icon: _isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.sm,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.md),
|
||||
)
|
||||
: Icon(widget.icon),
|
||||
onPressed: widget.disabled || _isLoading ? null : _onPressed,
|
||||
style: IconButton.styleFrom(backgroundColor: background, foregroundColor: foreground),
|
||||
icon: Icon(icon),
|
||||
onPressed: effectiveOnPressed,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class _ImmichPasswordInputState extends State<ImmichPasswordInput> {
|
||||
icon: Icon(_visible ? Icons.visibility_off_rounded : Icons.visibility_rounded),
|
||||
),
|
||||
autofillHints: [AutofillHints.password],
|
||||
keyboardType: TextInputType.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,85 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichTextButton extends StatefulWidget {
|
||||
class ImmichTextButton extends StatelessWidget {
|
||||
final String labelText;
|
||||
final IconData? icon;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool expanded;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
final bool? loading;
|
||||
|
||||
const ImmichTextButton({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
this.icon,
|
||||
required this.onPressed,
|
||||
this.variant = .filled,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.color = ImmichColor.primary,
|
||||
this.expanded = true,
|
||||
|
||||
this.loading = false,
|
||||
this.disabled = false,
|
||||
this.loading,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichTextButton> createState() => _ImmichTextButtonState();
|
||||
}
|
||||
Widget _buildButton(ImmichVariant variant) {
|
||||
final Widget? effectiveIcon = loading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.md,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg),
|
||||
)
|
||||
: icon != null
|
||||
? Icon(icon, fontWeight: FontWeight.w600)
|
||||
: null;
|
||||
final hasIcon = effectiveIcon != null;
|
||||
|
||||
class _ImmichTextButtonState extends State<ImmichTextButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
final label = Text(labelText, style: const TextStyle(fontSize: ImmichTextSize.body, fontWeight: FontWeight.bold));
|
||||
final style = ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: ImmichSpacing.md));
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
final effectiveOnPressed = disabled || loading ? null : onPressed;
|
||||
|
||||
switch (variant) {
|
||||
case ImmichVariant.filled:
|
||||
if (hasIcon) {
|
||||
return ElevatedButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
case ImmichVariant.ghost:
|
||||
if (hasIcon) {
|
||||
return TextButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return TextButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget? icon = _isLoading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.md,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg),
|
||||
)
|
||||
: widget.icon != null
|
||||
? Icon(widget.icon, fontWeight: .w600)
|
||||
: null;
|
||||
|
||||
final label = Text(
|
||||
widget.labelText,
|
||||
style: const .new(fontSize: ImmichTextSize.body, fontWeight: .bold),
|
||||
);
|
||||
final style = ElevatedButton.styleFrom(padding: const .symmetric(vertical: ImmichSpacing.md));
|
||||
final onPressed = widget.disabled || _isLoading ? null : _onPressed;
|
||||
|
||||
final button = switch (widget.variant) {
|
||||
ImmichVariant.filled => ElevatedButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
|
||||
ImmichVariant.ghost => TextButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
|
||||
};
|
||||
|
||||
if (widget.expanded) {
|
||||
final button = _buildButton(variant);
|
||||
if (expanded) {
|
||||
return SizedBox(width: double.infinity, child: button);
|
||||
}
|
||||
return button;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/color_override.dart';
|
||||
import 'package:immich_ui/src/translation.dart';
|
||||
|
||||
extension TranslationHelper on BuildContext {
|
||||
ImmichTranslations get translations => ImmichTranslationProvider.of(this);
|
||||
}
|
||||
|
||||
extension ColorHelper on BuildContext {
|
||||
Color? get colorOverride => ImmichColorOverride.maybeOf(this);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:immich_ui/src/previews.dart';
|
||||
import 'package:immich_ui/src/snackbar.dart';
|
||||
|
||||
@ImmichPreview(group: 'Snackbar', name: 'Types')
|
||||
Widget previewSnackbarTypes() => const _SnackbarDemo();
|
||||
|
||||
class _SnackbarDemo extends StatelessWidget {
|
||||
const _SnackbarDemo();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScaffoldMessenger(
|
||||
key: scaffoldMessengerKey,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Center(
|
||||
child: Wrap(
|
||||
spacing: ImmichSpacing.md,
|
||||
runSpacing: ImmichSpacing.md,
|
||||
children: [
|
||||
ElevatedButton(onPressed: () => snackbar.info('Info message'), child: const Text('Info')),
|
||||
ElevatedButton(onPressed: () => snackbar.success('Saved'), child: const Text('Success')),
|
||||
ElevatedButton(onPressed: () => snackbar.error('Something failed'), child: const Text('Error')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,16 @@ Widget previewTextButtonVariants() => const Wrap(
|
||||
],
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'Colors')
|
||||
Widget previewTextButtonColors() => const Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichTextButton(onPressed: _previewNoop, labelText: 'Primary', expanded: false),
|
||||
ImmichTextButton(onPressed: _previewNoop, labelText: 'Secondary', color: ImmichColor.secondary, expanded: false),
|
||||
],
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'With Icons')
|
||||
Widget previewTextButtonWithIcons() => const Wrap(
|
||||
spacing: 12,
|
||||
@@ -32,11 +42,7 @@ Widget previewTextButtonWithIcons() => const Wrap(
|
||||
);
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'Loading')
|
||||
Widget previewTextButtonLoading() => ImmichTextButton(
|
||||
onPressed: () => Future<void>.delayed(const Duration(seconds: 2)),
|
||||
labelText: 'Click me',
|
||||
expanded: false,
|
||||
);
|
||||
Widget previewTextButtonLoading() => const _PreviewLoadingDemo();
|
||||
|
||||
@ImmichPreview(group: 'TextButton', name: 'Disabled')
|
||||
Widget previewTextButtonDisabled() => const Wrap(
|
||||
@@ -53,3 +59,30 @@ Widget previewTextButtonDisabled() => const Wrap(
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
class _PreviewLoadingDemo extends StatefulWidget {
|
||||
const _PreviewLoadingDemo();
|
||||
|
||||
@override
|
||||
State<_PreviewLoadingDemo> createState() => _PreviewLoadingDemoState();
|
||||
}
|
||||
|
||||
class _PreviewLoadingDemoState extends State<_PreviewLoadingDemo> {
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImmichTextButton(
|
||||
onPressed: () async {
|
||||
setState(() => _isLoading = true);
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
},
|
||||
labelText: _isLoading ? 'Loading...' : 'Click Me',
|
||||
loading: _isLoading,
|
||||
expanded: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
class SnackbarManager {
|
||||
const SnackbarManager();
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(String message, SnackbarType type) {
|
||||
final messenger = scaffoldMessengerKey.currentState;
|
||||
final context = scaffoldMessengerKey.currentContext;
|
||||
if (messenger == null || context == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
messenger.hideCurrentSnackBar();
|
||||
return messenger.showSnackBar(_build(context, message, type));
|
||||
}
|
||||
|
||||
SnackBar _build(BuildContext context, String message, SnackbarType type) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.extension<ImmichColors>() ?? ImmichColors.harmonized(theme.colorScheme);
|
||||
final (IconData icon, Color background, Color foreground) = switch (type) {
|
||||
.info => (Icons.info_rounded, colors.info, colors.onInfo),
|
||||
.success => (Icons.check_circle_rounded, colors.success, colors.onSuccess),
|
||||
.error => (Icons.warning_rounded, colors.error, colors.onError),
|
||||
};
|
||||
|
||||
return SnackBar(
|
||||
behavior: .floating,
|
||||
backgroundColor: background,
|
||||
duration: const .new(seconds: 4),
|
||||
shape: const RoundedRectangleBorder(borderRadius: .all(.circular(ImmichRadius.sm))),
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(icon, color: foreground, size: ImmichIconSize.sm),
|
||||
const SizedBox(width: ImmichSpacing.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
maxLines: 2,
|
||||
overflow: .ellipsis,
|
||||
style: .new(color: foreground, fontWeight: .w600, fontSize: ImmichTextSize.body),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message) => show(message, .info);
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message) => show(message, .success);
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message) => show(message, .error);
|
||||
}
|
||||
|
||||
const snackbar = SnackbarManager();
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:material_color_utilities/blend/blend.dart';
|
||||
import 'package:material_color_utilities/hct/hct.dart';
|
||||
import 'package:material_color_utilities/palettes/tonal_palette.dart';
|
||||
|
||||
class ImmichThemeProvider extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
@@ -14,7 +11,6 @@ class ImmichThemeProvider extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
extensions: [ImmichColors.harmonized(colorScheme)],
|
||||
colorScheme: colorScheme,
|
||||
brightness: colorScheme.brightness,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
@@ -23,8 +19,8 @@ class ImmichThemeProvider extends StatelessWidget {
|
||||
final color = states.contains(WidgetState.error)
|
||||
? colorScheme.error
|
||||
: states.contains(WidgetState.focused)
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline;
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline;
|
||||
return OutlineInputBorder(
|
||||
borderSide: BorderSide(color: color),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
@@ -42,71 +38,3 @@ class ImmichThemeProvider extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichColors extends ThemeExtension<ImmichColors> {
|
||||
final Color info;
|
||||
final Color onInfo;
|
||||
final Color success;
|
||||
final Color onSuccess;
|
||||
final Color error;
|
||||
final Color onError;
|
||||
|
||||
const ImmichColors({
|
||||
required this.info,
|
||||
required this.onInfo,
|
||||
required this.success,
|
||||
required this.onSuccess,
|
||||
required this.error,
|
||||
required this.onError,
|
||||
});
|
||||
|
||||
factory ImmichColors.harmonized(ColorScheme scheme) {
|
||||
final (info, onInfo) = scheme.harmonized(const Color(0xFF1984E9));
|
||||
final (success, onSuccess) = scheme.harmonized(const Color(0xFF10C14D));
|
||||
final (error, onError) = scheme.harmonized(const Color(0xFFFA2921));
|
||||
return ImmichColors(
|
||||
info: info,
|
||||
onInfo: onInfo,
|
||||
success: success,
|
||||
onSuccess: onSuccess,
|
||||
error: error,
|
||||
onError: onError,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ImmichColors copyWith({Color? info, Color? onInfo, Color? success, Color? onSuccess, Color? error, Color? onError}) {
|
||||
return ImmichColors(
|
||||
info: info ?? this.info,
|
||||
onInfo: onInfo ?? this.onInfo,
|
||||
success: success ?? this.success,
|
||||
onSuccess: onSuccess ?? this.onSuccess,
|
||||
error: error ?? this.error,
|
||||
onError: onError ?? this.onError,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ImmichColors lerp(ImmichColors? other, double t) {
|
||||
if (other == null) {
|
||||
return this;
|
||||
}
|
||||
return ImmichColors(
|
||||
info: Color.lerp(info, other.info, t)!,
|
||||
onInfo: Color.lerp(onInfo, other.onInfo, t)!,
|
||||
success: Color.lerp(success, other.success, t)!,
|
||||
onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!,
|
||||
error: Color.lerp(error, other.error, t)!,
|
||||
onError: Color.lerp(onError, other.onError, t)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on ColorScheme {
|
||||
(Color container, Color onContainer) harmonized(Color seed) {
|
||||
final hct = Hct.fromInt(Blend.harmonize(seed.toARGB32(), primary.toARGB32()));
|
||||
final tones = TonalPalette.of(hct.hue, hct.chroma);
|
||||
final isDark = brightness == Brightness.dark;
|
||||
return (Color(tones.get(isDark ? 30 : 90)), Color(tones.get(isDark ? 90 : 10)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
enum ImmichVariant { filled, ghost }
|
||||
enum ImmichVariant {
|
||||
filled,
|
||||
ghost,
|
||||
}
|
||||
|
||||
enum ImmichColor { primary, secondary }
|
||||
|
||||
enum SnackbarType { info, success, error }
|
||||
enum ImmichColor {
|
||||
primary,
|
||||
secondary,
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
|
||||
@@ -7,7 +7,6 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
material_color_utilities: any
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_ui/src/color_override.dart';
|
||||
import 'package:immich_ui/src/components/icon_button.dart';
|
||||
|
||||
import 'test_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('ImmichColorOverride', () {
|
||||
testWidgets('exposes the override color to descendants', (tester) async {
|
||||
Color? captured;
|
||||
await tester.pumpTestWidget(
|
||||
ImmichColorOverride(
|
||||
color: Colors.green,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
captured = ImmichColorOverride.maybeOf(context);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(captured, Colors.green);
|
||||
});
|
||||
|
||||
testWidgets('maybeOf returns null when there is no override', (tester) async {
|
||||
Color? captured = Colors.black;
|
||||
await tester.pumpTestWidget(
|
||||
Builder(
|
||||
builder: (context) {
|
||||
captured = ImmichColorOverride.maybeOf(context);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(captured, isNull);
|
||||
});
|
||||
|
||||
testWidgets('a descendant component adopts the override as its foreground', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
ImmichColorOverride(
|
||||
color: Colors.green,
|
||||
child: ImmichIconButton(icon: Icons.add, onPressed: () {}),
|
||||
),
|
||||
);
|
||||
|
||||
final button = tester.widget<IconButton>(find.byType(IconButton));
|
||||
expect(button.style?.foregroundColor?.resolve(<WidgetState>{}), Colors.green);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_ui/src/snackbar.dart';
|
||||
|
||||
import 'test_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('SnackbarManager', () {
|
||||
testWidgets('shows the message', (tester) async {
|
||||
await tester.pumpTestWidget(const SizedBox());
|
||||
|
||||
snackbar.success('hello');
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('hello'), findsOneWidget);
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('replaces the current snackbar', (tester) async {
|
||||
await tester.pumpTestWidget(const SizedBox());
|
||||
|
||||
snackbar.info('first');
|
||||
await tester.pump();
|
||||
snackbar.error('second');
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('first'), findsNothing);
|
||||
expect(find.text('second'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('no-ops when the messenger is unmounted', (tester) async {
|
||||
expect(snackbar.show('x', .info), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_ui/src/snackbar.dart';
|
||||
|
||||
extension WidgetTesterExtension on WidgetTester {
|
||||
/// Pumps a widget wrapped in MaterialApp and Scaffold for testing.
|
||||
Future<void> pumpTestWidget(Widget widget) {
|
||||
return pumpWidget(
|
||||
MaterialApp(
|
||||
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||
home: Scaffold(body: widget),
|
||||
),
|
||||
);
|
||||
return pumpWidget(MaterialApp(home: Scaffold(body: widget)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,29 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/semver.dart';
|
||||
import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||
|
||||
void main() {
|
||||
group('app major version behind server', () {
|
||||
const message =
|
||||
'Your mobile app version is not compatible with the server! Please update your mobile app to the latest version.';
|
||||
test('getVersionCompatibilityMessage', () {
|
||||
String? result;
|
||||
|
||||
test('returns message when app major is behind server major', () {
|
||||
final result = getVersionCompatibilityMessage(
|
||||
const SemVer(major: 2, minor: 0, patch: 0),
|
||||
const SemVer(major: 1, minor: 200, patch: 0),
|
||||
);
|
||||
expect(result, message);
|
||||
});
|
||||
result = getVersionCompatibilityMessage(1, 106, 1, 105);
|
||||
expect(
|
||||
result,
|
||||
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
|
||||
);
|
||||
|
||||
test('returns null when app major matches server major', () {
|
||||
final result = getVersionCompatibilityMessage(
|
||||
const SemVer(major: 2, minor: 0, patch: 0),
|
||||
const SemVer(major: 2, minor: 0, patch: 0),
|
||||
);
|
||||
expect(result, null);
|
||||
});
|
||||
});
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 105);
|
||||
expect(
|
||||
result,
|
||||
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
|
||||
);
|
||||
|
||||
group('app major version too far ahead of server', () {
|
||||
const message =
|
||||
'Your server version is not compatible with the mobile app! Please update your server to the latest version.';
|
||||
result = getVersionCompatibilityMessage(1, 106, 1, 106);
|
||||
expect(result, null);
|
||||
|
||||
test('returns message when app major is more than one ahead of server', () {
|
||||
final result = getVersionCompatibilityMessage(
|
||||
const SemVer(major: 1, minor: 200, patch: 0),
|
||||
const SemVer(major: 3, minor: 0, patch: 0),
|
||||
);
|
||||
expect(result, message);
|
||||
});
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 106);
|
||||
expect(result, null);
|
||||
|
||||
test('returns null when app major is exactly one ahead of server', () {
|
||||
final result = getVersionCompatibilityMessage(
|
||||
const SemVer(major: 1, minor: 200, patch: 0),
|
||||
const SemVer(major: 2, minor: 0, patch: 0),
|
||||
);
|
||||
expect(result, null);
|
||||
});
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 108);
|
||||
expect(result, null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,9 +278,7 @@
|
||||
"title": "Album IDs",
|
||||
"array": true,
|
||||
"description": "Target album IDs",
|
||||
"uiHint": {
|
||||
"type": "AlbumId"
|
||||
}
|
||||
"uiHint": "AlbumId"
|
||||
},
|
||||
"albumName": {
|
||||
"type": "string",
|
||||
@@ -370,20 +368,14 @@
|
||||
"type": "string",
|
||||
"title": "Album ID",
|
||||
"description": "Target album ID",
|
||||
"uiHint": {
|
||||
"type": "AlbumId",
|
||||
"order": 1
|
||||
}
|
||||
"uiHint": "AlbumId"
|
||||
},
|
||||
"albumIds": {
|
||||
"type": "string",
|
||||
"title": "Album IDs",
|
||||
"description": "Target album IDs",
|
||||
"array": true,
|
||||
"uiHint": {
|
||||
"type": "AlbumId",
|
||||
"order": 2
|
||||
}
|
||||
"uiHint": "AlbumId"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ Değerli fotoğraflarınız ve videolarınız için daima [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) yedekleme planını uygulayın!
|
||||
>
|
||||
> ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
|
||||
>
|
||||
|
||||
> [!NOTE]
|
||||
> Kurulum dahil olmak üzere resmi belgeleri https://immich.app/ adresinde bulabilirsiniz.
|
||||
|
||||
@@ -14,12 +14,7 @@ const JsonSchemaPropertySchema = z
|
||||
enum: z.array(z.string()).optional().describe('Valid choices for enum types'),
|
||||
array: z.boolean().optional().describe('Type is an array type'),
|
||||
required: z.array(z.string()).optional().describe('A list of required properties'),
|
||||
uiHint: z
|
||||
.object({
|
||||
type: z.string().optional(),
|
||||
order: z.int().optional(),
|
||||
})
|
||||
.optional(),
|
||||
uiHint: z.string().optional(),
|
||||
get properties() {
|
||||
return z.record(z.string(), JsonSchemaPropertySchema).optional();
|
||||
},
|
||||
|
||||
@@ -129,7 +129,6 @@ from
|
||||
and "integrity_report"."type" = $1
|
||||
where
|
||||
"asset"."deletedAt" is null
|
||||
and "asset"."isExternal" = false
|
||||
and "integrity_report"."createdAt" >= $2
|
||||
and "integrity_report"."createdAt" <= $3
|
||||
order by
|
||||
|
||||
@@ -88,7 +88,7 @@ from
|
||||
where
|
||||
"album_asset"."updateId" < $3
|
||||
and "album_asset"."updateId" <= $4
|
||||
and "album_asset"."updateId" > $5
|
||||
and "album_asset"."updateId" >= $5
|
||||
and "album_asset"."albumId" = $6
|
||||
order by
|
||||
"album_asset"."updateId" asc
|
||||
@@ -202,7 +202,7 @@ from
|
||||
where
|
||||
"album_asset"."updateId" < $1
|
||||
and "album_asset"."updateId" <= $2
|
||||
and "album_asset"."updateId" > $3
|
||||
and "album_asset"."updateId" >= $3
|
||||
and "album_asset"."albumId" = $4
|
||||
order by
|
||||
"album_asset"."updateId" asc
|
||||
@@ -297,7 +297,7 @@ from
|
||||
where
|
||||
"album_asset"."updateId" < $1
|
||||
and "album_asset"."updateId" <= $2
|
||||
and "album_asset"."updateId" > $3
|
||||
and "album_asset"."updateId" >= $3
|
||||
and "album_asset"."albumId" = $4
|
||||
order by
|
||||
"album_asset"."updateId" asc
|
||||
@@ -349,7 +349,7 @@ from
|
||||
where
|
||||
"album_user"."updateId" < $1
|
||||
and "album_user"."updateId" <= $2
|
||||
and "album_user"."updateId" > $3
|
||||
and "album_user"."updateId" >= $3
|
||||
and "albumId" = $4
|
||||
order by
|
||||
"album_user"."updateId" asc
|
||||
@@ -810,7 +810,7 @@ from
|
||||
where
|
||||
"asset"."updateId" < $2
|
||||
and "asset"."updateId" <= $3
|
||||
and "asset"."updateId" > $4
|
||||
and "asset"."updateId" >= $4
|
||||
and "ownerId" = $5
|
||||
order by
|
||||
"asset"."updateId" asc
|
||||
@@ -908,7 +908,7 @@ from
|
||||
where
|
||||
"asset_exif"."updateId" < $1
|
||||
and "asset_exif"."updateId" <= $2
|
||||
and "asset_exif"."updateId" > $3
|
||||
and "asset_exif"."updateId" >= $3
|
||||
and "asset"."ownerId" = $4
|
||||
order by
|
||||
"asset_exif"."updateId" asc
|
||||
@@ -997,7 +997,7 @@ from
|
||||
where
|
||||
"stack"."updateId" < $1
|
||||
and "stack"."updateId" <= $2
|
||||
and "stack"."updateId" > $3
|
||||
and "stack"."updateId" >= $3
|
||||
and "ownerId" = $4
|
||||
order by
|
||||
"stack"."updateId" asc
|
||||
|
||||
@@ -177,7 +177,6 @@ export class IntegrityRepository {
|
||||
'asset.id as assetId',
|
||||
'integrity_report.id as reportId',
|
||||
])
|
||||
.where('asset.isExternal', '=', sql.lit(false))
|
||||
.$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')
|
||||
|
||||
@@ -106,7 +106,7 @@ export class BaseSync {
|
||||
.selectFrom(table(t).as(t))
|
||||
.where(updateIdRef, '<', nowId)
|
||||
.where(updateIdRef, '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (qb) => qb.where(updateIdRef, '>', afterUpdateId!))
|
||||
.$if(!!afterUpdateId, (qb) => qb.where(updateIdRef, '>=', afterUpdateId!))
|
||||
.orderBy(updateIdRef, 'asc');
|
||||
}
|
||||
|
||||
|
||||
@@ -2939,8 +2939,6 @@ describe(MediaService.name, () => {
|
||||
'7',
|
||||
'-global_quality:v',
|
||||
'23',
|
||||
'-b:v',
|
||||
'6897k',
|
||||
'-maxrate',
|
||||
'10000k',
|
||||
'-bufsize',
|
||||
|
||||
@@ -788,12 +788,6 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'}`, `${this.config.crf}`];
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0) {
|
||||
// Workaround for https://github.com/immich-app/immich/issues/29220, to be revisited
|
||||
// QSV seems to ignore -maxrate without -b:v
|
||||
// -b:v alongside global_quality uses QVBR
|
||||
if (!this.useCQP()) {
|
||||
options.push('-b:v', `${bitrates.target}${bitrates.unit}`);
|
||||
}
|
||||
options.push('-maxrate', `${bitrates.max}${bitrates.unit}`, '-bufsize', `${bitrates.max * 2}${bitrates.unit}`);
|
||||
}
|
||||
return options;
|
||||
|
||||
@@ -686,22 +686,6 @@ describe(IntegrityService.name, () => {
|
||||
nextCursor: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip external library files', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const job = ctx.getMock(JobRepository);
|
||||
job.queue.mockResolvedValue(void 0);
|
||||
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id, isExternal: true });
|
||||
|
||||
await sut.handleChecksumFiles({ refreshOnly: false });
|
||||
|
||||
await expect(
|
||||
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
|
||||
).resolves.toEqual({ items: [], nextCursor: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleChecksumRefresh', () => {
|
||||
|
||||
@@ -155,57 +155,6 @@ describe(SyncRequestType.AlbumToAssetsV1, () => {
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
});
|
||||
|
||||
it('should not resend an already-acked item when backfill resumes', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
|
||||
// backfill needs assets with an older updateId
|
||||
const { asset: sharedAsset1 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const { asset: sharedAsset2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
|
||||
await wait(2);
|
||||
|
||||
const { album: sharedAlbum } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: sharedAlbum.id, assetId: sharedAsset1.id });
|
||||
await ctx.newAlbumAsset({ albumId: sharedAlbum.id, assetId: sharedAsset2.id });
|
||||
|
||||
await wait(2);
|
||||
|
||||
// backfill needs an initial ack, otherwise it syncs everything
|
||||
const { asset: ownedAsset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { album: ownedAlbum } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: ownedAlbum.id, assetId: ownedAsset.id });
|
||||
|
||||
const setupResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
await ctx.syncAckAll(auth, setupResponse);
|
||||
|
||||
// share album to trigger backfill
|
||||
await ctx.newAlbumUser({ albumId: sharedAlbum.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
const response1 = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response1).toEqual([
|
||||
// receive both
|
||||
expect.objectContaining({ data: { albumId: sharedAlbum.id, assetId: sharedAsset1.id } }),
|
||||
expect.objectContaining({ data: { albumId: sharedAlbum.id, assetId: sharedAsset2.id } }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncAckV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
// ack 1st
|
||||
await ctx.sut.setAcks(auth, { acks: [response1[0].ack] });
|
||||
|
||||
const response2 = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response2).toEqual([
|
||||
// receive 2nd
|
||||
expect.objectContaining({ data: { albumId: sharedAlbum.id, assetId: sharedAsset2.id } }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncAckV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response2);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted album to asset relation', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const albumRepo = ctx.get(AlbumRepository);
|
||||
|
||||
@@ -279,68 +279,6 @@ describe(SyncRequestType.PartnerAssetsV2, () => {
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should not resend an already-acked item when backfill resumes', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
|
||||
// backfill needs assets with an older updateId
|
||||
const { asset: partnerAsset1 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await wait(2);
|
||||
const { asset: partnerAsset2 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
|
||||
await wait(2);
|
||||
|
||||
// backfill needs an initial ack, otherwise it syncs everything
|
||||
const { asset: initialAsset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const setupResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(setupResponse).toEqual([
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: initialAsset.id }),
|
||||
type: SyncEntityType.PartnerAssetV2,
|
||||
}),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.syncAckAll(auth, setupResponse);
|
||||
|
||||
// partner share to trigger backfill
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response1 = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(response1).toEqual([
|
||||
// receive both
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: partnerAsset1.id }),
|
||||
type: SyncEntityType.PartnerAssetBackfillV2,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: partnerAsset2.id }),
|
||||
type: SyncEntityType.PartnerAssetBackfillV2,
|
||||
}),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncAckV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
// ack 1st
|
||||
await ctx.sut.setAcks(auth, { acks: [response1[0].ack] });
|
||||
|
||||
const response2 = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(response2).toEqual([
|
||||
// receive 2nd
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ id: partnerAsset2.id }),
|
||||
type: SyncEntityType.PartnerAssetBackfillV2,
|
||||
}),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncAckV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response2);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should hide isFavorite for partner assets', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
|
||||
+1
-3
@@ -159,9 +159,7 @@
|
||||
}
|
||||
|
||||
.text-white-shadow {
|
||||
text-shadow:
|
||||
0 0 4px rgba(0, 0, 0, 0.9),
|
||||
0 1px 3px rgba(0, 0, 0, 0.8);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.icon-white-drop-shadow {
|
||||
|
||||
@@ -51,8 +51,6 @@
|
||||
};
|
||||
|
||||
const setUiHintValue = (values: string[]) => setValue(schema.array ? values : values[0]);
|
||||
const getSchemaProperties = (schema: JSONSchemaProperty) =>
|
||||
Object.entries(schema.properties ?? {}).sort((a, b) => (a[1].uiHint?.order ?? 0) - (b[1].uiHint?.order ?? 0));
|
||||
|
||||
const getBoolean = (defaultValue = false) => getValue<boolean>(defaultValue);
|
||||
const getString = () => getValue<string>();
|
||||
@@ -74,11 +72,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4 {root ? '' : 'border-l-4 border-gray-200 ps-2'}">
|
||||
{#each getSchemaProperties(schema) as [childKey, childSchema] (childKey)}
|
||||
{#each Object.entries(schema.properties ?? {}) as [childKey, childSchema] (childKey)}
|
||||
<Self schema={childSchema} key={childKey} bind:config={getValue, setValue} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if schema.uiHint?.type === 'AlbumId'}
|
||||
{:else if schema.uiHint === 'AlbumId'}
|
||||
<SchemaAlbumPicker {label} {description} array={schema.array} bind:albumIds={getUiHintValue, setUiHintValue} />
|
||||
{:else if schema.enum && schema.array}
|
||||
<Field {label} {description}>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Icon, IconButton, Link, LoadingSpinner, Text } from '@immich/ui';
|
||||
import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui';
|
||||
import { mdiCamera, mdiCameraIris, mdiClose, mdiImageOutline, mdiInformationOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -310,13 +310,14 @@
|
||||
{#snippet popup({ marker })}
|
||||
{@const { lat, lon } = marker}
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Text fontWeight="bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</Text>
|
||||
<Link
|
||||
<p class="font-bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</p>
|
||||
<a
|
||||
href="https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=13#map=15/{lat}/{lon}"
|
||||
class="text-primary"
|
||||
target="_blank"
|
||||
class="font-medium text-primary underline focus:outline-none"
|
||||
>
|
||||
{$t('open_in_openstreetmap')}
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Map>
|
||||
|
||||
@@ -324,18 +324,6 @@
|
||||
shortcut: { key: ' ' },
|
||||
onShortcut: () => (videoPlayer?.paused ? videoPlayer?.play() : videoPlayer?.pause()),
|
||||
},
|
||||
{
|
||||
shortcut: { shift: true, key: 'ArrowLeft' },
|
||||
onShortcut: () =>
|
||||
videoPlayer ? (videoPlayer.currentTime = Math.max(videoPlayer.currentTime - 0.4, 0)) : undefined,
|
||||
},
|
||||
{
|
||||
shortcut: { shift: true, key: 'ArrowRight' },
|
||||
onShortcut: () =>
|
||||
videoPlayer
|
||||
? (videoPlayer.currentTime = Math.min(videoPlayer.currentTime + 0.4, videoPlayer.duration))
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
|
||||
{#if !!assetOwner}
|
||||
<div class="absolute inset-e-2 bottom-1 z-2 max-w-[50%]">
|
||||
<p class="text-white-shadow max-w-full truncate p-1 text-xs font-medium text-white">
|
||||
<p class="max-w-full truncate text-xs font-medium text-white drop-shadow-lg">
|
||||
{assetOwner.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
Control,
|
||||
ControlButton,
|
||||
ControlGroup,
|
||||
FullscreenControl,
|
||||
GeoJSON,
|
||||
GeolocateControl,
|
||||
MapLibre,
|
||||
@@ -342,6 +343,7 @@
|
||||
|
||||
{#if !simplified}
|
||||
<GeolocateControl position="top-left" />
|
||||
<FullscreenControl position="top-left" />
|
||||
<ScaleControl />
|
||||
<AttributionControl compact={false} />
|
||||
{/if}
|
||||
@@ -399,13 +401,13 @@
|
||||
>
|
||||
{#snippet children({ feature }: { feature: Feature })}
|
||||
{#if useLocationPin}
|
||||
<Icon icon={mdiMapMarker} size="50px" class="translate-y-[calc(5px-50%)] text-primary" />
|
||||
<Icon icon={mdiMapMarker} size="50px" class="translate-y-[-50%] text-primary" />
|
||||
{:else}
|
||||
<img
|
||||
src={getAssetMediaUrl({ id: feature.properties?.id })}
|
||||
class="size-15 rounded-full border-2 border-immich-primary bg-immich-primary object-cover shadow-lg transition-all duration-200 hover:scale-150 hover:border-immich-dark-primary"
|
||||
alt={feature.properties?.city && feature.properties.country
|
||||
? $t('map_marker_for_image', {
|
||||
? $t('map_marker_for_images', {
|
||||
values: { city: feature.properties.city, country: feature.properties.country },
|
||||
})
|
||||
: $t('map_marker_with_image')}
|
||||
@@ -413,7 +415,7 @@
|
||||
{/if}
|
||||
{#if popup}
|
||||
<Popup offset={[0, -30]} openOn="click" closeOnClickOutside>
|
||||
{@render popup({ marker: asMarker(feature) })}
|
||||
{@render popup?.({ marker: asMarker(feature) })}
|
||||
</Popup>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -24,8 +24,7 @@ class FaceManager {
|
||||
});
|
||||
|
||||
readonly people = $derived.by(() => {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const people = new Map<string, PersonResponseDto>();
|
||||
const people = new SvelteMap<string, PersonResponseDto>();
|
||||
|
||||
for (const face of this.data) {
|
||||
if (face.person) {
|
||||
@@ -33,7 +32,7 @@ class FaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(people.values());
|
||||
return people.values();
|
||||
});
|
||||
|
||||
readonly facesByPersonId = $derived.by(() => {
|
||||
|
||||
@@ -96,10 +96,7 @@ export type JSONSchemaProperty = {
|
||||
array?: boolean;
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
uiHint?: {
|
||||
type?: 'AlbumId' | 'AssetId' | 'PersonId';
|
||||
order?: number;
|
||||
};
|
||||
uiHint?: 'AlbumId' | 'AssetId' | 'PersonId';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -169,9 +169,7 @@
|
||||
preload={false}
|
||||
/>
|
||||
{#if person.name}
|
||||
<span
|
||||
class="text-white-shadow absolute inset-s-0 bottom-2 w-full px-1 text-center font-medium text-white select-text"
|
||||
>
|
||||
<span class="absolute inset-s-0 bottom-2 w-full px-1 text-center font-medium text-white select-text">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
);
|
||||
const isGhost = $derived(step.id === 'ghost');
|
||||
|
||||
const getUiHint = (key: string) => schema?.properties?.[key]?.uiHint?.type;
|
||||
const getUiHint = (key: string) => schema?.properties?.[key]?.uiHint;
|
||||
const toIds = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : [String(value)]);
|
||||
let dragImage = $state<Element>();
|
||||
let isDropTarget = $state(false);
|
||||
|
||||
Reference in New Issue
Block a user