Compare commits

..

18 Commits

Author SHA1 Message Date
bo0tzz
2f2e281b07 empty commit to allow PR? 2025-04-03 13:48:20 +02:00
Zack Pollard
40cff2893c fix: metadata service init failure should halt server startup (#17356) 2025-04-03 12:35:39 +01:00
Abhinav Valecha
b621281351 feat(server): Avoid face match with people born after file creation #4743 (#16918)
* feat(server): Avoid face matching with people born after file creation date (#4743)

* lint

* add medium tests for facial recognition

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-02 10:37:26 -05:00
Snowknight26
4336afd6bf fix(web): fix thumbnail hover link position (#16762)
* fix(web): don't show a scrollbar when hovering over the last row of images on the search page

* Format code

* Fix asset selection z-index

* Remove anchor overlay on mouseover

* Fix a test

* Fix merge

* Fix overlays

* fix merge

* fix stack thumbs in asset viewer

* fix dimmed bounds, animation

* lint

---------

Co-authored-by: Min Idzelis <min123@gmail.com>
2025-04-02 10:30:41 -05:00
shenlong
5a456ef277 feat(mobile): sqlite (#16861)
* refactor: user entity

* chore: rebase fixes

* refactor: remove int user Id

* refactor: migrate store userId from int to string

* refactor: rename uid to id

* feat: drift

* pr feedback

* refactor: move common overrides to mixin

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-04-02 08:58:17 -05:00
renovate[bot]
5cb5fcbf62 fix(deps): update machine-learning (#17286) 2025-04-02 03:57:19 +00:00
Alex
95e3b15776 fix(web): padding (#17320) 2025-04-01 23:32:54 -04:00
Ben
50335dc363 fix(web): menu button size (#17321)
Adjusting the menu button size, to match match the other buttons in the navigation bar.
2025-04-01 22:25:17 -05:00
Ben
6e62c09d84 feat(web): expand/collapse sidebar (#16768)
* feat: expand/collapse sidebar

* fix: general PR cleanup

- add skip link unit test
- remove unused tailwind styles
- adjust asset grid spacing
- fix event propogation

* fix: cleaning up event listeners

* fix: purchase modal and button on small screens

* fix: explicit tailwind classes

* fix: no animation on initial page load

* fix: sidebar spacing and reactivity

* chore: reverting changes to icons in nav and account info panel

* fix: remove left margin from the asset grid after merging in new timeline

* chore: extract search-bar changes for a separate PR

* fix: add margin to memories
2025-04-01 21:12:04 -05:00
github-actions
00d3b8d83a chore: version v1.131.3 2025-04-01 22:27:52 +00:00
Mert
d911b76c08 fix(server): use stat instead of exifinfo for file date metadata (#17311)
* use stat instead of filecreatedate

* update tests

* unused import
2025-04-01 17:24:07 -05:00
shenlong
502854cee1 fix(server): remove stacks on stack.deleteAll (#17288)
* fix(server): delete all stacks on deleteAll

* remove unnecessary assets update

* generate sql

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-04-01 17:20:43 -05:00
Mert
59e5c82569 fix(server): full-size images not migrated or deleted correctly (#17308)
* fix file path logic

* update tests

* add empty array fallback just in case for now
2025-04-01 18:11:46 -04:00
Daimolean
e4b0c00885 fix(web): select all button displays incorrectly (#17305)
* fix(web): select all show incorrectly

* fix: lint

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-04-01 19:00:48 +00:00
Alex
946507231d fix(web): blank locale cause blank timeline to render (#17284)
* fix(web): blank locale cause blank timeline to render

* correct fix

* newline

* pr feedback
2025-04-01 18:58:11 +00:00
Alex
20ba800a50 fix(web): date time change reactivity (#17306)
* fix(web): date time change reactivity

* remove logs
2025-04-01 18:57:53 +00:00
Alex
f434e858ed fix(mobile): getAllByRemoteId return all assets on empty arguments value (#17263)
* chore: post release tasks

* fix(mobile): getAllByRemoteId return all assets if ids is empty
2025-04-01 08:59:21 -05:00
bo0tzz
3e03c47fbf fix: strip extra metadata when transcoding (#17297) 2025-04-01 08:58:59 -05:00
101 changed files with 3293 additions and 576 deletions

3
.gitattributes vendored
View File

@@ -6,6 +6,9 @@ mobile/openapi/**/*.dart linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true
mobile/lib/**/*.drift.dart -diff -merge
mobile/lib/**/*.drift.dart linguist-generated=true
open-api/typescript-sdk/fetch-client.ts -diff -merge
open-api/typescript-sdk/fetch-client.ts linguist-generated=true

View File

@@ -39,6 +39,7 @@
],
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
}
}

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.60",
"version": "2.2.61",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.60",
"version": "2.2.61",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"chokidar": "^4.0.3",
@@ -54,7 +54,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.131.2",
"version": "1.131.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.60",
"version": "2.2.61",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -1,4 +1,8 @@
[
{
"label": "v1.131.3",
"url": "https://v1.131.3.archive.immich.app"
},
{
"label": "v1.131.2",
"url": "https://v1.131.2.archive.immich.app"

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.131.2",
"version": "1.131.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.131.2",
"version": "1.131.3",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -44,7 +44,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.60",
"version": "2.2.61",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -93,7 +93,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.131.2",
"version": "1.131.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.131.2",
"version": "1.131.3",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -864,6 +864,7 @@
"loop_videos": "Loop videos",
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
"main_branch_warning": "Youre using a development version; we strongly recommend using a release version!",
"main_menu": "Main menu",
"make": "Make",
"manage_shared_links": "Manage shared links",
"manage_sharing_with_partners": "Manage sharing with partners",

View File

@@ -54,7 +54,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
RUN apt-get update && apt-get install -y --no-install-recommends g++
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:57da96c4557243fc0a732817854084e81af9393f64dc7d172f39c16465b5e2ba /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:fb91e82e8643382d5bce074ba0d167677d678faff4bd518dac670476d19b159c /uv /uvx /bin/
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \

309
machine-learning/uv.lock generated
View File

@@ -508,16 +508,16 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.115.11"
version = "0.115.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/28/c5d26e5860df807241909a961a37d45e10533acef95fc368066c7dd186cd/fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f", size = 294441 }
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/5d/4d8bbb94f0dbc22732350c06965e40740f4a92ca560e90bb566f4f73af41/fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64", size = 94926 },
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 },
]
[[package]]
@@ -1583,62 +1583,64 @@ wheels = [
[[package]]
name = "orjson"
version = "3.10.15"
version = "3.10.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 }
sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532 },
{ url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229 },
{ url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148 },
{ url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748 },
{ url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559 },
{ url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349 },
{ url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514 },
{ url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940 },
{ url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713 },
{ url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028 },
{ url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715 },
{ url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473 },
{ url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564 },
{ url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533 },
{ url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230 },
{ url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148 },
{ url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749 },
{ url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558 },
{ url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349 },
{ url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513 },
{ url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942 },
{ url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717 },
{ url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033 },
{ url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720 },
{ url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473 },
{ url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570 },
{ url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 },
{ url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 },
{ url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 },
{ url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 },
{ url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 },
{ url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 },
{ url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 },
{ url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 },
{ url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 },
{ url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 },
{ url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 },
{ url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 },
{ url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 },
{ url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 },
{ url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 },
{ url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 },
{ url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 },
{ url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 },
{ url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 },
{ url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 },
{ url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 },
{ url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 },
{ url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 },
{ url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 },
{ url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 },
{ url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 },
{ url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179 },
{ url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510 },
{ url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373 },
{ url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774 },
{ url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030 },
{ url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677 },
{ url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798 },
{ url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450 },
{ url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356 },
{ url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769 },
{ url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223 },
{ url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734 },
{ url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779 },
{ url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180 },
{ url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510 },
{ url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373 },
{ url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773 },
{ url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029 },
{ url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677 },
{ url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800 },
{ url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451 },
{ url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358 },
{ url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772 },
{ url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225 },
{ url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733 },
{ url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784 },
{ url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325 },
{ url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621 },
{ url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270 },
{ url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346 },
{ url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845 },
{ url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078 },
{ url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712 },
{ url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136 },
{ url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258 },
{ url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326 },
{ url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800 },
{ url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516 },
{ url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759 },
{ url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944 },
{ url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289 },
{ url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640 },
{ url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286 },
{ url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307 },
{ url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739 },
{ url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076 },
{ url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643 },
{ url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168 },
{ url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271 },
{ url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444 },
{ url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737 },
{ url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482 },
{ url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714 },
{ url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 },
]
[[package]]
@@ -1787,91 +1789,104 @@ wheels = [
[[package]]
name = "pydantic"
version = "2.10.6"
version = "2.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
{ url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
version = "2.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 },
{ url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 },
{ url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 },
{ url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 },
{ url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 },
{ url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 },
{ url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 },
{ url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 },
{ url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 },
{ url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 },
{ url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 },
{ url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 },
{ url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 },
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
{ url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 },
{ url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 },
{ url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 },
{ url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 },
{ url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 },
{ url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 },
{ url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 },
{ url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 },
{ url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 },
{ url = "https://files.pythonhosted.org/packages/29/43/0649ad07e66b36a3fb21442b425bd0348ac162c5e686b36471f363201535/pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e", size = 2042968 },
{ url = "https://files.pythonhosted.org/packages/a0/a6/975fea4774a459e495cb4be288efd8b041ac756a0a763f0b976d0861334b/pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518", size = 1860347 },
{ url = "https://files.pythonhosted.org/packages/aa/49/7858dadad305101a077ec4d0c606b6425a2b134ea8d858458a6d287fd871/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73", size = 1910060 },
{ url = "https://files.pythonhosted.org/packages/8d/4f/6522527911d9c5fe6d76b084d8b388d5c84b09d113247b39f91937500b34/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207", size = 1997129 },
{ url = "https://files.pythonhosted.org/packages/75/d0/06f396da053e3d73001ea4787e56b4d7132a87c0b5e2e15a041e808c35cd/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b", size = 2140389 },
{ url = "https://files.pythonhosted.org/packages/f5/6b/b9ff5b69cd4ef007cf665463f3be2e481dc7eb26c4a55b2f57a94308c31a/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f", size = 2754237 },
{ url = "https://files.pythonhosted.org/packages/53/80/b4879de375cdf3718d05fcb60c9aa1f119d28e261dafa51b6a69c78f7178/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5", size = 2007433 },
{ url = "https://files.pythonhosted.org/packages/46/24/54054713dc0af98a94eab37e0f4294dfd5cd8f70b2ca9dcdccd15709fd7e/pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276", size = 2123980 },
{ url = "https://files.pythonhosted.org/packages/3a/4c/257c1cb89e14cfa6e95ebcb91b308eb1dd2b348340ff76a6e6fcfa9969e1/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760", size = 2087433 },
{ url = "https://files.pythonhosted.org/packages/0c/62/927df8a39ad78ef7b82c5446e01dec9bb0043e1ad71d8f426062f5f014db/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa", size = 2260242 },
{ url = "https://files.pythonhosted.org/packages/74/f2/389414f7c77a100954e84d6f52a82bd1788ae69db72364376d8a73b38765/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c", size = 2258227 },
{ url = "https://files.pythonhosted.org/packages/53/99/94516313e15d906a1264bb40faf24a01a4af4e2ca8a7c10dd173b6513c5a/pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025", size = 1925523 },
{ url = "https://files.pythonhosted.org/packages/7d/67/cc789611c6035a0b71305a1ec6ba196256ced76eba8375f316f840a70456/pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc", size = 1951872 },
{ url = "https://files.pythonhosted.org/packages/f0/93/9e97af2619b4026596487a79133e425c7d3c374f0a7f100f3d76bcdf9c83/pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef", size = 2042784 },
{ url = "https://files.pythonhosted.org/packages/42/b4/0bba8412fd242729feeb80e7152e24f0e1a1c19f4121ca3d4a307f4e6222/pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a", size = 1858179 },
{ url = "https://files.pythonhosted.org/packages/69/1f/c1c40305d929bd08af863df64b0a26203b70b352a1962d86f3bcd52950fe/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b", size = 1909396 },
{ url = "https://files.pythonhosted.org/packages/0f/99/d2e727375c329c1e652b5d450fbb9d56e8c3933a397e4bd46e67c68c2cd5/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd", size = 1998264 },
{ url = "https://files.pythonhosted.org/packages/9c/2e/3119a33931278d96ecc2e9e1b9d50c240636cfeb0c49951746ae34e4de74/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b", size = 2140588 },
{ url = "https://files.pythonhosted.org/packages/35/bd/9267bd1ba55f17c80ef6cb7e07b3890b4acbe8eb6014f3102092d53d9300/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d", size = 2746296 },
{ url = "https://files.pythonhosted.org/packages/6f/ed/ef37de6478a412ee627cbebd73e7b72a680f45bfacce9ff1199de6e17e88/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd", size = 2005555 },
{ url = "https://files.pythonhosted.org/packages/dd/84/72c8d1439585d8ee7bc35eb8f88a04a4d302ee4018871f1f85ae1b0c6625/pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453", size = 2124452 },
{ url = "https://files.pythonhosted.org/packages/a7/8f/cb13de30c6a3e303423751a529a3d1271c2effee4b98cf3e397a66ae8498/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b", size = 2087001 },
{ url = "https://files.pythonhosted.org/packages/83/d0/e93dc8884bf288a63fedeb8040ac8f29cb71ca52e755f48e5170bb63e55b/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915", size = 2261663 },
{ url = "https://files.pythonhosted.org/packages/4c/ba/4b7739c95efa0b542ee45fd872c8f6b1884ab808cf04ce7ac6621b6df76e/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2", size = 2257786 },
{ url = "https://files.pythonhosted.org/packages/cc/98/73cbca1d2360c27752cfa2fcdcf14d96230e92d7d48ecd50499865c56bf7/pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86", size = 1925697 },
{ url = "https://files.pythonhosted.org/packages/9a/26/d85a40edeca5d8830ffc33667d6fef329fd0f4bc0c5181b8b0e206cfe488/pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b", size = 1949859 },
{ url = "https://files.pythonhosted.org/packages/7e/0b/5a381605f0b9870465b805f2c86c06b0a7c191668ebe4117777306c2c1e5/pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a", size = 1907978 },
{ url = "https://files.pythonhosted.org/packages/a9/c4/c9381323cbdc1bb26d352bc184422ce77c4bc2f2312b782761093a59fafc/pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43", size = 2025127 },
{ url = "https://files.pythonhosted.org/packages/6f/bd/af35278080716ecab8f57e84515c7dc535ed95d1c7f52c1c6f7b313a9dab/pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd", size = 1851687 },
{ url = "https://files.pythonhosted.org/packages/12/e4/a01461225809c3533c23bd1916b1e8c2e21727f0fea60ab1acbffc4e2fca/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6", size = 1892232 },
{ url = "https://files.pythonhosted.org/packages/51/17/3d53d62a328fb0a49911c2962036b9e7a4f781b7d15e9093c26299e5f76d/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6", size = 1977896 },
{ url = "https://files.pythonhosted.org/packages/30/98/01f9d86e02ec4a38f4b02086acf067f2c776b845d43f901bd1ee1c21bc4b/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4", size = 2127717 },
{ url = "https://files.pythonhosted.org/packages/3c/43/6f381575c61b7c58b0fd0b92134c5a1897deea4cdfc3d47567b3ff460a4e/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61", size = 2680287 },
{ url = "https://files.pythonhosted.org/packages/01/42/c0d10d1451d161a9a0da9bbef023b8005aa26e9993a8cc24dc9e3aa96c93/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862", size = 2008276 },
{ url = "https://files.pythonhosted.org/packages/20/ca/e08df9dba546905c70bae44ced9f3bea25432e34448d95618d41968f40b7/pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a", size = 2115305 },
{ url = "https://files.pythonhosted.org/packages/03/1f/9b01d990730a98833113581a78e595fd40ed4c20f9693f5a658fb5f91eff/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099", size = 2068999 },
{ url = "https://files.pythonhosted.org/packages/20/18/fe752476a709191148e8b1e1139147841ea5d2b22adcde6ee6abb6c8e7cf/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6", size = 2241488 },
{ url = "https://files.pythonhosted.org/packages/81/22/14738ad0a0bf484b928c9e52004f5e0b81dd8dabbdf23b843717b37a71d1/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3", size = 2248430 },
{ url = "https://files.pythonhosted.org/packages/e8/27/be7571e215ac8d321712f2433c445b03dbcd645366a18f67b334df8912bc/pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2", size = 1908353 },
{ url = "https://files.pythonhosted.org/packages/be/3a/be78f28732f93128bd0e3944bdd4b3970b389a1fbd44907c97291c8dcdec/pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48", size = 1955956 },
{ url = "https://files.pythonhosted.org/packages/21/26/b8911ac74faa994694b76ee6a22875cc7a4abea3c381fdba4edc6c6bef84/pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6", size = 1903259 },
{ url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 },
{ url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 },
{ url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 },
{ url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 },
{ url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 },
{ url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 },
{ url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 },
{ url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 },
{ url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 },
{ url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 },
{ url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 },
{ url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 },
{ url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 },
{ url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 },
{ url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 },
{ url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 },
{ url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 },
{ url = "https://files.pythonhosted.org/packages/44/77/85e173b715e1a277ce934f28d877d82492df13e564fa68a01c96f36a47ad/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50", size = 2040129 },
{ url = "https://files.pythonhosted.org/packages/33/e7/33da5f8a94bbe2191cfcd15bd6d16ecd113e67da1b8c78d3cc3478112dab/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c", size = 1872656 },
{ url = "https://files.pythonhosted.org/packages/b4/7a/9600f222bea840e5b9ba1f17c0acc79b669b24542a78c42c6a10712c0aae/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1", size = 1903731 },
{ url = "https://files.pythonhosted.org/packages/81/d2/94c7ca4e24c5dcfb74df92e0836c189e9eb6814cf62d2f26a75ea0a906db/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b", size = 2083966 },
{ url = "https://files.pythonhosted.org/packages/b8/74/a0259989d220e8865ed6866a6d40539e40fa8f507e587e35d2414cc081f8/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d", size = 2118951 },
{ url = "https://files.pythonhosted.org/packages/13/4c/87405ed04d6d07597920b657f082a8e8e58bf3034178bb9044b4d57a91e2/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442", size = 2079632 },
{ url = "https://files.pythonhosted.org/packages/5a/4c/bcb02970ef91d4cd6de7c6893101302637da456bc8b52c18ea0d047b55ce/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a", size = 2250541 },
{ url = "https://files.pythonhosted.org/packages/a3/2b/dbe5450c4cd904be5da736dcc7f2357b828199e29e38de19fc81f988b288/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330", size = 2255685 },
{ url = "https://files.pythonhosted.org/packages/ca/a6/ca1d35f695d81f639c5617fc9efb44caad21a9463383fa45364b3044175a/pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae", size = 2082395 },
{ url = "https://files.pythonhosted.org/packages/2b/b2/553e42762e7b08771fca41c0230c1ac276f9e79e78f57628e1b7d328551d/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c", size = 2041207 },
{ url = "https://files.pythonhosted.org/packages/85/81/a91a57bbf3efe53525ab75f65944b8950e6ef84fe3b9a26c1ec173363263/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db", size = 1873736 },
{ url = "https://files.pythonhosted.org/packages/9c/d2/5ab52e9f551cdcbc1ee99a0b3ef595f56d031f66f88e5ca6726c49f9ce65/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b", size = 1903794 },
{ url = "https://files.pythonhosted.org/packages/2f/5f/a81742d3f3821b16f1265f057d6e0b68a3ab13a814fe4bffac536a1f26fd/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9", size = 2083457 },
{ url = "https://files.pythonhosted.org/packages/b5/2f/e872005bc0fc47f9c036b67b12349a8522d32e3bda928e82d676e2a594d1/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c", size = 2119537 },
{ url = "https://files.pythonhosted.org/packages/d3/13/183f13ce647202eaf3dada9e42cdfc59cbb95faedd44d25f22b931115c7f/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976", size = 2080069 },
{ url = "https://files.pythonhosted.org/packages/23/8b/b6be91243da44a26558d9c3a9007043b3750334136c6550551e8092d6d96/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c", size = 2251618 },
{ url = "https://files.pythonhosted.org/packages/aa/c5/fbcf1977035b834f63eb542e74cd6c807177f383386175b468f0865bcac4/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936", size = 2255374 },
{ url = "https://files.pythonhosted.org/packages/2f/f8/66f328e411f1c9574b13c2c28ab01f308b53688bbbe6ca8fb981e6cabc42/pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8", size = 2082099 },
]
[[package]]
@@ -1933,14 +1948,14 @@ wheels = [
[[package]]
name = "pytest-asyncio"
version = "0.25.3"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 }
sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 },
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 },
]
[[package]]
@@ -2444,23 +2459,23 @@ wheels = [
[[package]]
name = "types-pyyaml"
version = "6.0.12.20241230"
version = "6.0.12.20250402"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078 }
sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029 },
{ url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 },
]
[[package]]
name = "types-requests"
version = "2.32.0.20250306"
version = "2.32.0.20250328"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/1a/beaeff79ef9efd186566ba5f0d95b44ae21f6d31e9413bcfbef3489b6ae3/types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1", size = 23012 }
sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/26/645d89f56004aa0ba3b96fec27793e3c7e62b40982ee069e52568922b6db/types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b", size = 20673 },
{ url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 },
]
[[package]]
@@ -2477,20 +2492,20 @@ wheels = [
[[package]]
name = "types-simplejson"
version = "3.20.0.20250318"
version = "3.20.0.20250326"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/bb/c19c0863ce03b950a2e6319e668a3e3f51c977368d6a1da529893e794162/types_simplejson-3.20.0.20250318.tar.gz", hash = "sha256:5ada2caa2f76826a90b97985f7b0caf55088a23ed4eb9c39fc1f5cb00b985e09", size = 9966 }
sdist = { url = "https://files.pythonhosted.org/packages/af/14/e26fc55e1ea56f9ea470917d3e2f8240e6d043ca914181021d04115ae0f7/types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2", size = 10489 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/5a/adb04301014b18324ec88d5fcfe0ffc262ffc68c443cfe79e32b6cc68a35/types_simplejson-3.20.0.20250318-py3-none-any.whl", hash = "sha256:e8c9cdb06b566b6ca1c7bf3d4c97fbd69a6f6a5aea760ef127f31e638a094c26", size = 10336 },
{ url = "https://files.pythonhosted.org/packages/76/bf/d3f3a5ba47fd18115e8446d39f025b85905d2008677c29ee4d03b4cddd57/types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7", size = 10462 },
]
[[package]]
name = "types-ujson"
version = "5.10.0.20240515"
version = "5.10.0.20250326"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/12/49/abb4bcb9f2258f785edbf236b517c3e7ba8a503a8cbce6b5895930586cc0/types-ujson-5.10.0.20240515.tar.gz", hash = "sha256:ceae7127f0dafe4af5dd0ecf98ee13e9d75951ef963b5c5a9b7ea92e0d71f0d7", size = 3571 }
sdist = { url = "https://files.pythonhosted.org/packages/cc/5c/c974451c4babdb4ae3588925487edde492d59a8403010b4642a554d09954/types_ujson-5.10.0.20250326.tar.gz", hash = "sha256:5469e05f2c31ecb3c4c0267cc8fe41bcd116826fbb4ded69801a645c687dd014", size = 8340 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/1f/9d018cee3d09ab44a5211f0b5ed9b0422ad9a8c226bf3967f5884498d8f0/types_ujson-5.10.0.20240515-py3-none-any.whl", hash = "sha256:02bafc36b3a93d2511757a64ff88bd505e0a57fba08183a9150fbcfcb2015310", size = 2757 },
{ url = "https://files.pythonhosted.org/packages/3e/c9/8a73a5f8fa6e70fc02eed506d5ac0ae9ceafbd2b8c9ad34a7de0f29900d6/types_ujson-5.10.0.20250326-py3-none-any.whl", hash = "sha256:acc0913f569def62ef6a892c8a47703f65d05669a3252391a97765cf207dca5b", size = 7644 },
]
[[package]]
@@ -2502,6 +2517,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "typing-inspection"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
]
[[package]]
name = "urllib3"
version = "2.1.0"

View File

@@ -36,6 +36,8 @@ analyzer:
exclude:
- openapi/**
- lib/generated_plugin_registrant.dart
- lib/**/*.g.dart
- lib/**/*.drift.dart
plugins:
- custom_lint

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 192,
"android.injected.version.name" => "1.131.2",
"android.injected.version.code" => 193,
"android.injected.version.name" => "1.131.3",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

24
mobile/build.yaml Normal file
View File

@@ -0,0 +1,24 @@
targets:
$default:
builders:
#drift @DriftDatabase()
drift_dev:
# Disable default builder to use modular builder instead
enabled: false
drift_dev:analyzer:
enabled: true
options: &drift_options
store_date_time_values_as_text: true
named_parameters: true
write_from_json_string_constructor: false
data_class_to_companions: false
# Required for make-migrations
databases:
main: lib/infrastructure/repositories/db.repository.dart
generate_for: &drift_generate_for
- lib/infrastructure/entities/*.dart
- lib/infrastructure/repositories/db.repository.dart
drift_dev:modular:
enabled: true
options: *drift_options
generate_for: *drift_generate_for

View File

@@ -0,0 +1 @@
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}}]}

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
version_number: "1.131.2"
version_number: "1.131.3"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -1,32 +1,4 @@
import 'dart:ui';
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary,
pink,
red,
yellow,
blue,
green,
purple,
orange,
gray,
amber;
Color toColor({bool isDarkTheme = false}) => switch (this) {
AvatarColor.primary =>
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
};
}
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
// TODO: Rename to User once Isar is removed
class UserDto {

View File

@@ -0,0 +1,105 @@
import 'dart:ui';
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary("primary"),
pink("pink"),
red("red"),
yellow("yellow"),
blue("blue"),
green("green"),
purple("purple"),
orange("orange"),
gray("gray"),
amber("amber");
final String value;
const AvatarColor(this.value);
Color toColor({bool isDarkTheme = false}) => switch (this) {
AvatarColor.primary =>
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
};
}
class UserPreferences {
final bool foldersEnabled;
final bool memoriesEnabled;
final bool peopleEnabled;
final bool ratingsEnabled;
final bool sharedLinksEnabled;
final bool tagsEnabled;
final AvatarColor userAvatarColor;
final bool showSupportBadge;
const UserPreferences({
this.foldersEnabled = false,
this.memoriesEnabled = true,
this.peopleEnabled = true,
this.ratingsEnabled = false,
this.sharedLinksEnabled = true,
this.tagsEnabled = false,
this.userAvatarColor = AvatarColor.primary,
this.showSupportBadge = true,
});
UserPreferences copyWith({
bool? foldersEnabled,
bool? memoriesEnabled,
bool? peopleEnabled,
bool? ratingsEnabled,
bool? sharedLinksEnabled,
bool? tagsEnabled,
AvatarColor? userAvatarColor,
bool? showSupportBadge,
}) {
return UserPreferences(
foldersEnabled: foldersEnabled ?? this.foldersEnabled,
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
peopleEnabled: peopleEnabled ?? this.peopleEnabled,
ratingsEnabled: ratingsEnabled ?? this.ratingsEnabled,
sharedLinksEnabled: sharedLinksEnabled ?? this.sharedLinksEnabled,
tagsEnabled: tagsEnabled ?? this.tagsEnabled,
userAvatarColor: userAvatarColor ?? this.userAvatarColor,
showSupportBadge: showSupportBadge ?? this.showSupportBadge,
);
}
Map<String, Object?> toMap() {
final preferences = <String, Object?>{};
preferences["folders-Enabled"] = foldersEnabled;
preferences["memories-Enabled"] = memoriesEnabled;
preferences["people-Enabled"] = peopleEnabled;
preferences["ratings-Enabled"] = ratingsEnabled;
preferences["sharedLinks-Enabled"] = sharedLinksEnabled;
preferences["tags-Enabled"] = tagsEnabled;
preferences["avatar-Color"] = userAvatarColor.value;
preferences["purchase-ShowSupportBadge"] = showSupportBadge;
return preferences;
}
factory UserPreferences.fromMap(Map<String, Object?> map) {
return UserPreferences(
foldersEnabled: map["folders-Enabled"] as bool? ?? false,
memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
peopleEnabled: map["people-Enabled"] as bool? ?? true,
ratingsEnabled: map["ratings-Enabled"] as bool? ?? false,
sharedLinksEnabled: map["sharedLinks-Enabled"] as bool? ?? true,
tagsEnabled: map["tags-Enabled"] as bool? ?? false,
userAvatarColor: AvatarColor.values.firstWhere(
(e) => e.value == map["avatar-Color"] as String?,
orElse: () => AvatarColor.primary,
),
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class PartnerEntity extends Table with DriftDefaultsMixin {
const PartnerEntity();
BlobColumn get sharedById =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
BlobColumn get sharedWithId =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
BoolColumn get inTimeline => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {sharedById, sharedWithId};
}

View File

@@ -0,0 +1,610 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion
Function({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
i0.Value<bool> inTimeline,
});
typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion
Function({
i0.Value<i2.Uint8List> sharedById,
i0.Value<i2.Uint8List> sharedWithId,
i0.Value<bool> inTimeline,
});
final class $$PartnerEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase, i1.$PartnerEntityTable, i1.PartnerEntityData> {
$$PartnerEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$PartnerEntityTable>('partner_entity')
.sharedById,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get sharedById {
final $_column = $_itemColumn<i2.Uint8List>('shared_by_id')!;
final manager = i5
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$PartnerEntityTable>('partner_entity')
.sharedWithId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get sharedWithId {
final $_column = $_itemColumn<i2.Uint8List>('shared_with_id')!;
final manager = i5
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$PartnerEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$PartnerEntityTable> {
$$PartnerEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<bool> get inTimeline => $composableBuilder(
column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column));
i5.$$UserEntityTableFilterComposer get sharedById {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$UserEntityTableFilterComposer get sharedWithId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$PartnerEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$PartnerEntityTable> {
$$PartnerEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<bool> get inTimeline => $composableBuilder(
column: $table.inTimeline,
builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get sharedById {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$UserEntityTableOrderingComposer get sharedWithId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$PartnerEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$PartnerEntityTable> {
$$PartnerEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<bool> get inTimeline => $composableBuilder(
column: $table.inTimeline, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get sharedById {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$UserEntityTableAnnotationComposer get sharedWithId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$PartnerEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$PartnerEntityTable,
i1.PartnerEntityData,
i1.$$PartnerEntityTableFilterComposer,
i1.$$PartnerEntityTableOrderingComposer,
i1.$$PartnerEntityTableAnnotationComposer,
$$PartnerEntityTableCreateCompanionBuilder,
$$PartnerEntityTableUpdateCompanionBuilder,
(i1.PartnerEntityData, i1.$$PartnerEntityTableReferences),
i1.PartnerEntityData,
i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})> {
$$PartnerEntityTableTableManager(
i0.GeneratedDatabase db, i1.$PartnerEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$PartnerEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$PartnerEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> sharedById = const i0.Value.absent(),
i0.Value<i2.Uint8List> sharedWithId = const i0.Value.absent(),
i0.Value<bool> inTimeline = const i0.Value.absent(),
}) =>
i1.PartnerEntityCompanion(
sharedById: sharedById,
sharedWithId: sharedWithId,
inTimeline: inTimeline,
),
createCompanionCallback: ({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
i0.Value<bool> inTimeline = const i0.Value.absent(),
}) =>
i1.PartnerEntityCompanion.insert(
sharedById: sharedById,
sharedWithId: sharedWithId,
inTimeline: inTimeline,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable(table),
i1.$$PartnerEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: ({sharedById = false, sharedWithId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (sharedById) {
state = state.withJoin(
currentTable: table,
currentColumn: table.sharedById,
referencedTable:
i1.$$PartnerEntityTableReferences._sharedByIdTable(db),
referencedColumn: i1.$$PartnerEntityTableReferences
._sharedByIdTable(db)
.id,
) as T;
}
if (sharedWithId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.sharedWithId,
referencedTable: i1.$$PartnerEntityTableReferences
._sharedWithIdTable(db),
referencedColumn: i1.$$PartnerEntityTableReferences
._sharedWithIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$PartnerEntityTable,
i1.PartnerEntityData,
i1.$$PartnerEntityTableFilterComposer,
i1.$$PartnerEntityTableOrderingComposer,
i1.$$PartnerEntityTableAnnotationComposer,
$$PartnerEntityTableCreateCompanionBuilder,
$$PartnerEntityTableUpdateCompanionBuilder,
(i1.PartnerEntityData, i1.$$PartnerEntityTableReferences),
i1.PartnerEntityData,
i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>;
class $PartnerEntityTable extends i3.PartnerEntity
with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$PartnerEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _sharedByIdMeta =
const i0.VerificationMeta('sharedById');
@override
late final i0.GeneratedColumn<i2.Uint8List> sharedById =
i0.GeneratedColumn<i2.Uint8List>('shared_by_id', aliasedName, false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _sharedWithIdMeta =
const i0.VerificationMeta('sharedWithId');
@override
late final i0.GeneratedColumn<i2.Uint8List> sharedWithId =
i0.GeneratedColumn<i2.Uint8List>('shared_with_id', aliasedName, false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _inTimelineMeta =
const i0.VerificationMeta('inTimeline');
@override
late final i0.GeneratedColumn<bool> inTimeline = i0.GeneratedColumn<bool>(
'in_timeline', aliasedName, false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("in_timeline" IN (0, 1))'),
defaultValue: const i4.Constant(false));
@override
List<i0.GeneratedColumn> get $columns =>
[sharedById, sharedWithId, inTimeline];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'partner_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.PartnerEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('shared_by_id')) {
context.handle(
_sharedByIdMeta,
sharedById.isAcceptableOrUnknown(
data['shared_by_id']!, _sharedByIdMeta));
} else if (isInserting) {
context.missing(_sharedByIdMeta);
}
if (data.containsKey('shared_with_id')) {
context.handle(
_sharedWithIdMeta,
sharedWithId.isAcceptableOrUnknown(
data['shared_with_id']!, _sharedWithIdMeta));
} else if (isInserting) {
context.missing(_sharedWithIdMeta);
}
if (data.containsKey('in_timeline')) {
context.handle(
_inTimelineMeta,
inTimeline.isAcceptableOrUnknown(
data['in_timeline']!, _inTimelineMeta));
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {sharedById, sharedWithId};
@override
i1.PartnerEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.PartnerEntityData(
sharedById: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!,
sharedWithId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!,
inTimeline: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!,
);
}
@override
$PartnerEntityTable createAlias(String alias) {
return $PartnerEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class PartnerEntityData extends i0.DataClass
implements i0.Insertable<i1.PartnerEntityData> {
final i2.Uint8List sharedById;
final i2.Uint8List sharedWithId;
final bool inTimeline;
const PartnerEntityData(
{required this.sharedById,
required this.sharedWithId,
required this.inTimeline});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById);
map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId);
map['in_timeline'] = i0.Variable<bool>(inTimeline);
return map;
}
factory PartnerEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return PartnerEntityData(
sharedById: serializer.fromJson<i2.Uint8List>(json['sharedById']),
sharedWithId: serializer.fromJson<i2.Uint8List>(json['sharedWithId']),
inTimeline: serializer.fromJson<bool>(json['inTimeline']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'sharedById': serializer.toJson<i2.Uint8List>(sharedById),
'sharedWithId': serializer.toJson<i2.Uint8List>(sharedWithId),
'inTimeline': serializer.toJson<bool>(inTimeline),
};
}
i1.PartnerEntityData copyWith(
{i2.Uint8List? sharedById,
i2.Uint8List? sharedWithId,
bool? inTimeline}) =>
i1.PartnerEntityData(
sharedById: sharedById ?? this.sharedById,
sharedWithId: sharedWithId ?? this.sharedWithId,
inTimeline: inTimeline ?? this.inTimeline,
);
PartnerEntityData copyWithCompanion(i1.PartnerEntityCompanion data) {
return PartnerEntityData(
sharedById:
data.sharedById.present ? data.sharedById.value : this.sharedById,
sharedWithId: data.sharedWithId.present
? data.sharedWithId.value
: this.sharedWithId,
inTimeline:
data.inTimeline.present ? data.inTimeline.value : this.inTimeline,
);
}
@override
String toString() {
return (StringBuffer('PartnerEntityData(')
..write('sharedById: $sharedById, ')
..write('sharedWithId: $sharedWithId, ')
..write('inTimeline: $inTimeline')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById),
i0.$driftBlobEquality.hash(sharedWithId), inTimeline);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.PartnerEntityData &&
i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) &&
i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) &&
other.inTimeline == this.inTimeline);
}
class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
final i0.Value<i2.Uint8List> sharedById;
final i0.Value<i2.Uint8List> sharedWithId;
final i0.Value<bool> inTimeline;
const PartnerEntityCompanion({
this.sharedById = const i0.Value.absent(),
this.sharedWithId = const i0.Value.absent(),
this.inTimeline = const i0.Value.absent(),
});
PartnerEntityCompanion.insert({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
this.inTimeline = const i0.Value.absent(),
}) : sharedById = i0.Value(sharedById),
sharedWithId = i0.Value(sharedWithId);
static i0.Insertable<i1.PartnerEntityData> custom({
i0.Expression<i2.Uint8List>? sharedById,
i0.Expression<i2.Uint8List>? sharedWithId,
i0.Expression<bool>? inTimeline,
}) {
return i0.RawValuesInsertable({
if (sharedById != null) 'shared_by_id': sharedById,
if (sharedWithId != null) 'shared_with_id': sharedWithId,
if (inTimeline != null) 'in_timeline': inTimeline,
});
}
i1.PartnerEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? sharedById,
i0.Value<i2.Uint8List>? sharedWithId,
i0.Value<bool>? inTimeline}) {
return i1.PartnerEntityCompanion(
sharedById: sharedById ?? this.sharedById,
sharedWithId: sharedWithId ?? this.sharedWithId,
inTimeline: inTimeline ?? this.inTimeline,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (sharedById.present) {
map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById.value);
}
if (sharedWithId.present) {
map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId.value);
}
if (inTimeline.present) {
map['in_timeline'] = i0.Variable<bool>(inTimeline.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('PartnerEntityCompanion(')
..write('sharedById: $sharedById, ')
..write('sharedWithId: $sharedWithId, ')
..write('inTimeline: $inTimeline')
..write(')'))
.toString();
}
}

View File

@@ -1,4 +1,7 @@
import 'package:drift/drift.dart' hide Index;
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
@@ -71,3 +74,20 @@ class User {
quotaSizeInBytes: quotaSizeInBytes,
);
}
class UserEntity extends Table with DriftDefaultsMixin {
const UserEntity();
BlobColumn get id => blob()();
TextColumn get name => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
TextColumn get email => text()();
TextColumn get profileImagePath => text().nullable()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
// Quota
IntColumn get quotaSizeInBytes => integer().nullable()();
IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {id};
}

View File

@@ -0,0 +1,656 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion
Function({
required i2.Uint8List id,
required String name,
i0.Value<bool> isAdmin,
required String email,
i0.Value<String?> profileImagePath,
i0.Value<DateTime> updatedAt,
i0.Value<int?> quotaSizeInBytes,
i0.Value<int> quotaUsageInBytes,
});
typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion
Function({
i0.Value<i2.Uint8List> id,
i0.Value<String> name,
i0.Value<bool> isAdmin,
i0.Value<String> email,
i0.Value<String?> profileImagePath,
i0.Value<DateTime> updatedAt,
i0.Value<int?> quotaSizeInBytes,
i0.Value<int> quotaUsageInBytes,
});
class $$UserEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$UserEntityTable> {
$$UserEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<i2.Uint8List> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get name => $composableBuilder(
column: $table.name, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get email => $composableBuilder(
column: $table.email, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get profileImagePath => $composableBuilder(
column: $table.profileImagePath,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => i0.ColumnFilters(column));
}
class $$UserEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$UserEntityTable> {
$$UserEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<i2.Uint8List> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get name => $composableBuilder(
column: $table.name, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get email => $composableBuilder(
column: $table.email, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get profileImagePath => $composableBuilder(
column: $table.profileImagePath,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => i0.ColumnOrderings(column));
}
class $$UserEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$UserEntityTable> {
$$UserEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<i2.Uint8List> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
i0.GeneratedColumn<bool> get isAdmin =>
$composableBuilder(column: $table.isAdmin, builder: (column) => column);
i0.GeneratedColumn<String> get email =>
$composableBuilder(column: $table.email, builder: (column) => column);
i0.GeneratedColumn<String> get profileImagePath => $composableBuilder(
column: $table.profileImagePath, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i0.GeneratedColumn<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes, builder: (column) => column);
i0.GeneratedColumn<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes, builder: (column) => column);
}
class $$UserEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$UserEntityTable,
i1.UserEntityData,
i1.$$UserEntityTableFilterComposer,
i1.$$UserEntityTableOrderingComposer,
i1.$$UserEntityTableAnnotationComposer,
$$UserEntityTableCreateCompanionBuilder,
$$UserEntityTableUpdateCompanionBuilder,
(
i1.UserEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$UserEntityTable,
i1.UserEntityData>
),
i1.UserEntityData,
i0.PrefetchHooks Function()> {
$$UserEntityTableTableManager(
i0.GeneratedDatabase db, i1.$UserEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$UserEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$UserEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$UserEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<String> email = const i0.Value.absent(),
i0.Value<String?> profileImagePath = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> quotaSizeInBytes = const i0.Value.absent(),
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
}) =>
i1.UserEntityCompanion(
id: id,
name: name,
isAdmin: isAdmin,
email: email,
profileImagePath: profileImagePath,
updatedAt: updatedAt,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
),
createCompanionCallback: ({
required i2.Uint8List id,
required String name,
i0.Value<bool> isAdmin = const i0.Value.absent(),
required String email,
i0.Value<String?> profileImagePath = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> quotaSizeInBytes = const i0.Value.absent(),
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
}) =>
i1.UserEntityCompanion.insert(
id: id,
name: name,
isAdmin: isAdmin,
email: email,
profileImagePath: profileImagePath,
updatedAt: updatedAt,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$UserEntityTable,
i1.UserEntityData,
i1.$$UserEntityTableFilterComposer,
i1.$$UserEntityTableOrderingComposer,
i1.$$UserEntityTableAnnotationComposer,
$$UserEntityTableCreateCompanionBuilder,
$$UserEntityTableUpdateCompanionBuilder,
(
i1.UserEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$UserEntityTable,
i1.UserEntityData>
),
i1.UserEntityData,
i0.PrefetchHooks Function()>;
class $UserEntityTable extends i3.UserEntity
with i0.TableInfo<$UserEntityTable, i1.UserEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$UserEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<i2.Uint8List> id =
i0.GeneratedColumn<i2.Uint8List>('id', aliasedName, false,
type: i0.DriftSqlType.blob, requiredDuringInsert: true);
static const i0.VerificationMeta _nameMeta =
const i0.VerificationMeta('name');
@override
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
'name', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _isAdminMeta =
const i0.VerificationMeta('isAdmin');
@override
late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
'is_admin', aliasedName, false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints:
i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'),
defaultValue: const i4.Constant(false));
static const i0.VerificationMeta _emailMeta =
const i0.VerificationMeta('email');
@override
late final i0.GeneratedColumn<String> email = i0.GeneratedColumn<String>(
'email', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _profileImagePathMeta =
const i0.VerificationMeta('profileImagePath');
@override
late final i0.GeneratedColumn<String> profileImagePath =
i0.GeneratedColumn<String>('profile_image_path', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _updatedAtMeta =
const i0.VerificationMeta('updatedAt');
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
static const i0.VerificationMeta _quotaSizeInBytesMeta =
const i0.VerificationMeta('quotaSizeInBytes');
@override
late final i0.GeneratedColumn<int> quotaSizeInBytes = i0.GeneratedColumn<int>(
'quota_size_in_bytes', aliasedName, true,
type: i0.DriftSqlType.int, requiredDuringInsert: false);
static const i0.VerificationMeta _quotaUsageInBytesMeta =
const i0.VerificationMeta('quotaUsageInBytes');
@override
late final i0.GeneratedColumn<int> quotaUsageInBytes =
i0.GeneratedColumn<int>('quota_usage_in_bytes', aliasedName, false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0));
@override
List<i0.GeneratedColumn> get $columns => [
id,
name,
isAdmin,
email,
profileImagePath,
updatedAt,
quotaSizeInBytes,
quotaUsageInBytes
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'user_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.UserEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('name')) {
context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('is_admin')) {
context.handle(_isAdminMeta,
isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta));
}
if (data.containsKey('email')) {
context.handle(
_emailMeta, email.isAcceptableOrUnknown(data['email']!, _emailMeta));
} else if (isInserting) {
context.missing(_emailMeta);
}
if (data.containsKey('profile_image_path')) {
context.handle(
_profileImagePathMeta,
profileImagePath.isAcceptableOrUnknown(
data['profile_image_path']!, _profileImagePathMeta));
}
if (data.containsKey('updated_at')) {
context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
}
if (data.containsKey('quota_size_in_bytes')) {
context.handle(
_quotaSizeInBytesMeta,
quotaSizeInBytes.isAcceptableOrUnknown(
data['quota_size_in_bytes']!, _quotaSizeInBytesMeta));
}
if (data.containsKey('quota_usage_in_bytes')) {
context.handle(
_quotaUsageInBytesMeta,
quotaUsageInBytes.isAcceptableOrUnknown(
data['quota_usage_in_bytes']!, _quotaUsageInBytesMeta));
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.UserEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.UserEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!,
name: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
isAdmin: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_admin'])!,
email: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}email'])!,
profileImagePath: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}profile_image_path']),
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
quotaSizeInBytes: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']),
quotaUsageInBytes: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!,
);
}
@override
$UserEntityTable createAlias(String alias) {
return $UserEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class UserEntityData extends i0.DataClass
implements i0.Insertable<i1.UserEntityData> {
final i2.Uint8List id;
final String name;
final bool isAdmin;
final String email;
final String? profileImagePath;
final DateTime updatedAt;
final int? quotaSizeInBytes;
final int quotaUsageInBytes;
const UserEntityData(
{required this.id,
required this.name,
required this.isAdmin,
required this.email,
this.profileImagePath,
required this.updatedAt,
this.quotaSizeInBytes,
required this.quotaUsageInBytes});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<i2.Uint8List>(id);
map['name'] = i0.Variable<String>(name);
map['is_admin'] = i0.Variable<bool>(isAdmin);
map['email'] = i0.Variable<String>(email);
if (!nullToAbsent || profileImagePath != null) {
map['profile_image_path'] = i0.Variable<String>(profileImagePath);
}
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
if (!nullToAbsent || quotaSizeInBytes != null) {
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes);
}
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes);
return map;
}
factory UserEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserEntityData(
id: serializer.fromJson<i2.Uint8List>(json['id']),
name: serializer.fromJson<String>(json['name']),
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
email: serializer.fromJson<String>(json['email']),
profileImagePath: serializer.fromJson<String?>(json['profileImagePath']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
quotaSizeInBytes: serializer.fromJson<int?>(json['quotaSizeInBytes']),
quotaUsageInBytes: serializer.fromJson<int>(json['quotaUsageInBytes']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<i2.Uint8List>(id),
'name': serializer.toJson<String>(name),
'isAdmin': serializer.toJson<bool>(isAdmin),
'email': serializer.toJson<String>(email),
'profileImagePath': serializer.toJson<String?>(profileImagePath),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'quotaSizeInBytes': serializer.toJson<int?>(quotaSizeInBytes),
'quotaUsageInBytes': serializer.toJson<int>(quotaUsageInBytes),
};
}
i1.UserEntityData copyWith(
{i2.Uint8List? id,
String? name,
bool? isAdmin,
String? email,
i0.Value<String?> profileImagePath = const i0.Value.absent(),
DateTime? updatedAt,
i0.Value<int?> quotaSizeInBytes = const i0.Value.absent(),
int? quotaUsageInBytes}) =>
i1.UserEntityData(
id: id ?? this.id,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
email: email ?? this.email,
profileImagePath: profileImagePath.present
? profileImagePath.value
: this.profileImagePath,
updatedAt: updatedAt ?? this.updatedAt,
quotaSizeInBytes: quotaSizeInBytes.present
? quotaSizeInBytes.value
: this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
);
UserEntityData copyWithCompanion(i1.UserEntityCompanion data) {
return UserEntityData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
email: data.email.present ? data.email.value : this.email,
profileImagePath: data.profileImagePath.present
? data.profileImagePath.value
: this.profileImagePath,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
quotaSizeInBytes: data.quotaSizeInBytes.present
? data.quotaSizeInBytes.value
: this.quotaSizeInBytes,
quotaUsageInBytes: data.quotaUsageInBytes.present
? data.quotaUsageInBytes.value
: this.quotaUsageInBytes,
);
}
@override
String toString() {
return (StringBuffer('UserEntityData(')
..write('id: $id, ')
..write('name: $name, ')
..write('isAdmin: $isAdmin, ')
..write('email: $email, ')
..write('profileImagePath: $profileImagePath, ')
..write('updatedAt: $updatedAt, ')
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
..write('quotaUsageInBytes: $quotaUsageInBytes')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin,
email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserEntityData &&
i0.$driftBlobEquality.equals(other.id, this.id) &&
other.name == this.name &&
other.isAdmin == this.isAdmin &&
other.email == this.email &&
other.profileImagePath == this.profileImagePath &&
other.updatedAt == this.updatedAt &&
other.quotaSizeInBytes == this.quotaSizeInBytes &&
other.quotaUsageInBytes == this.quotaUsageInBytes);
}
class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
final i0.Value<i2.Uint8List> id;
final i0.Value<String> name;
final i0.Value<bool> isAdmin;
final i0.Value<String> email;
final i0.Value<String?> profileImagePath;
final i0.Value<DateTime> updatedAt;
final i0.Value<int?> quotaSizeInBytes;
final i0.Value<int> quotaUsageInBytes;
const UserEntityCompanion({
this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.isAdmin = const i0.Value.absent(),
this.email = const i0.Value.absent(),
this.profileImagePath = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.quotaSizeInBytes = const i0.Value.absent(),
this.quotaUsageInBytes = const i0.Value.absent(),
});
UserEntityCompanion.insert({
required i2.Uint8List id,
required String name,
this.isAdmin = const i0.Value.absent(),
required String email,
this.profileImagePath = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.quotaSizeInBytes = const i0.Value.absent(),
this.quotaUsageInBytes = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
email = i0.Value(email);
static i0.Insertable<i1.UserEntityData> custom({
i0.Expression<i2.Uint8List>? id,
i0.Expression<String>? name,
i0.Expression<bool>? isAdmin,
i0.Expression<String>? email,
i0.Expression<String>? profileImagePath,
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? quotaSizeInBytes,
i0.Expression<int>? quotaUsageInBytes,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (isAdmin != null) 'is_admin': isAdmin,
if (email != null) 'email': email,
if (profileImagePath != null) 'profile_image_path': profileImagePath,
if (updatedAt != null) 'updated_at': updatedAt,
if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes,
if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes,
});
}
i1.UserEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? id,
i0.Value<String>? name,
i0.Value<bool>? isAdmin,
i0.Value<String>? email,
i0.Value<String?>? profileImagePath,
i0.Value<DateTime>? updatedAt,
i0.Value<int?>? quotaSizeInBytes,
i0.Value<int>? quotaUsageInBytes}) {
return i1.UserEntityCompanion(
id: id ?? this.id,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
email: email ?? this.email,
profileImagePath: profileImagePath ?? this.profileImagePath,
updatedAt: updatedAt ?? this.updatedAt,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<i2.Uint8List>(id.value);
}
if (name.present) {
map['name'] = i0.Variable<String>(name.value);
}
if (isAdmin.present) {
map['is_admin'] = i0.Variable<bool>(isAdmin.value);
}
if (email.present) {
map['email'] = i0.Variable<String>(email.value);
}
if (profileImagePath.present) {
map['profile_image_path'] = i0.Variable<String>(profileImagePath.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
if (quotaSizeInBytes.present) {
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes.value);
}
if (quotaUsageInBytes.present) {
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('UserEntityCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('isAdmin: $isAdmin, ')
..write('email: $email, ')
..write('profileImagePath: $profileImagePath, ')
..write('updatedAt: $updatedAt, ')
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
..write('quotaUsageInBytes: $quotaUsageInBytes')
..write(')'))
.toString();
}
}

View File

@@ -0,0 +1,21 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class UserMetadataEntity extends Table with DriftDefaultsMixin {
const UserMetadataEntity();
BlobColumn get userId =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get preferences => text().map(userPreferenceConverter)();
@override
Set<Column> get primaryKey => {userId};
}
final JsonTypeConverter2<UserPreferences, String, Object?>
userPreferenceConverter = TypeConverter.json2(
fromJson: (json) => UserPreferences.fromMap(json as Map<String, Object?>),
toJson: (pref) => pref.toMap(),
);

View File

@@ -0,0 +1,468 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'
as i4;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$UserMetadataEntityTableCreateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
});
typedef $$UserMetadataEntityTableUpdateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
i0.Value<i2.Uint8List> userId,
i0.Value<i3.UserPreferences> preferences,
});
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase,
i1.$UserMetadataEntityTable,
i1.UserMetadataEntityData> {
$$UserMetadataEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$UserMetadataEntityTable>(
'user_metadata_entity')
.userId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<i2.Uint8List>('user_id')!;
final manager = i5
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_userIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$UserMetadataEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$UserMetadataEntityTable> {
$$UserMetadataEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnWithTypeConverterFilters<i3.UserPreferences, i3.UserPreferences,
String>
get preferences => $composableBuilder(
column: $table.preferences,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i5.$$UserEntityTableFilterComposer get userId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$UserMetadataEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$UserMetadataEntityTable> {
$$UserMetadataEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get preferences => $composableBuilder(
column: $table.preferences,
builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get userId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$UserMetadataEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$UserMetadataEntityTable> {
$$UserMetadataEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
get preferences => $composableBuilder(
column: $table.preferences, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get userId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$UserMetadataEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$UserMetadataEntityTable,
i1.UserMetadataEntityData,
i1.$$UserMetadataEntityTableFilterComposer,
i1.$$UserMetadataEntityTableOrderingComposer,
i1.$$UserMetadataEntityTableAnnotationComposer,
$$UserMetadataEntityTableCreateCompanionBuilder,
$$UserMetadataEntityTableUpdateCompanionBuilder,
(i1.UserMetadataEntityData, i1.$$UserMetadataEntityTableReferences),
i1.UserMetadataEntityData,
i0.PrefetchHooks Function({bool userId})> {
$$UserMetadataEntityTableTableManager(
i0.GeneratedDatabase db, i1.$UserMetadataEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () => i1
.$$UserMetadataEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$UserMetadataEntityTableOrderingComposer(
$db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$UserMetadataEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> userId = const i0.Value.absent(),
i0.Value<i3.UserPreferences> preferences = const i0.Value.absent(),
}) =>
i1.UserMetadataEntityCompanion(
userId: userId,
preferences: preferences,
),
createCompanionCallback: ({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
}) =>
i1.UserMetadataEntityCompanion.insert(
userId: userId,
preferences: preferences,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable(table),
i1.$$UserMetadataEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: ({userId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (userId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.userId,
referencedTable:
i1.$$UserMetadataEntityTableReferences._userIdTable(db),
referencedColumn: i1.$$UserMetadataEntityTableReferences
._userIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $$UserMetadataEntityTableProcessedTableManager
= i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$UserMetadataEntityTable,
i1.UserMetadataEntityData,
i1.$$UserMetadataEntityTableFilterComposer,
i1.$$UserMetadataEntityTableOrderingComposer,
i1.$$UserMetadataEntityTableAnnotationComposer,
$$UserMetadataEntityTableCreateCompanionBuilder,
$$UserMetadataEntityTableUpdateCompanionBuilder,
(i1.UserMetadataEntityData, i1.$$UserMetadataEntityTableReferences),
i1.UserMetadataEntityData,
i0.PrefetchHooks Function({bool userId})>;
class $UserMetadataEntityTable extends i4.UserMetadataEntity
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$UserMetadataEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _userIdMeta =
const i0.VerificationMeta('userId');
@override
late final i0.GeneratedColumn<i2.Uint8List> userId =
i0.GeneratedColumn<i2.Uint8List>('user_id', aliasedName, false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@override
late final i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
preferences = i0.GeneratedColumn<String>(
'preferences', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true)
.withConverter<i3.UserPreferences>(
i1.$UserMetadataEntityTable.$converterpreferences);
@override
List<i0.GeneratedColumn> get $columns => [userId, preferences];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'user_metadata_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.UserMetadataEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('user_id')) {
context.handle(_userIdMeta,
userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta));
} else if (isInserting) {
context.missing(_userIdMeta);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {userId};
@override
i1.UserMetadataEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.UserMetadataEntityData(
userId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!,
preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
);
}
@override
$UserMetadataEntityTable createAlias(String alias) {
return $UserMetadataEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i3.UserPreferences, String, Object?>
$converterpreferences = i4.userPreferenceConverter;
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class UserMetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.UserMetadataEntityData> {
final i2.Uint8List userId;
final i3.UserPreferences preferences;
const UserMetadataEntityData(
{required this.userId, required this.preferences});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['user_id'] = i0.Variable<i2.Uint8List>(userId);
{
map['preferences'] = i0.Variable<String>(
i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
}
return map;
}
factory UserMetadataEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserMetadataEntityData(
userId: serializer.fromJson<i2.Uint8List>(json['userId']),
preferences: i1.$UserMetadataEntityTable.$converterpreferences
.fromJson(serializer.fromJson<Object?>(json['preferences'])),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<i2.Uint8List>(userId),
'preferences': serializer.toJson<Object?>(i1
.$UserMetadataEntityTable.$converterpreferences
.toJson(preferences)),
};
}
i1.UserMetadataEntityData copyWith(
{i2.Uint8List? userId, i3.UserPreferences? preferences}) =>
i1.UserMetadataEntityData(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
);
UserMetadataEntityData copyWithCompanion(
i1.UserMetadataEntityCompanion data) {
return UserMetadataEntityData(
userId: data.userId.present ? data.userId.value : this.userId,
preferences:
data.preferences.present ? data.preferences.value : this.preferences,
);
}
@override
String toString() {
return (StringBuffer('UserMetadataEntityData(')
..write('userId: $userId, ')
..write('preferences: $preferences')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(i0.$driftBlobEquality.hash(userId), preferences);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserMetadataEntityData &&
i0.$driftBlobEquality.equals(other.userId, this.userId) &&
other.preferences == this.preferences);
}
class UserMetadataEntityCompanion
extends i0.UpdateCompanion<i1.UserMetadataEntityData> {
final i0.Value<i2.Uint8List> userId;
final i0.Value<i3.UserPreferences> preferences;
const UserMetadataEntityCompanion({
this.userId = const i0.Value.absent(),
this.preferences = const i0.Value.absent(),
});
UserMetadataEntityCompanion.insert({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
}) : userId = i0.Value(userId),
preferences = i0.Value(preferences);
static i0.Insertable<i1.UserMetadataEntityData> custom({
i0.Expression<i2.Uint8List>? userId,
i0.Expression<String>? preferences,
}) {
return i0.RawValuesInsertable({
if (userId != null) 'user_id': userId,
if (preferences != null) 'preferences': preferences,
});
}
i1.UserMetadataEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? userId,
i0.Value<i3.UserPreferences>? preferences}) {
return i1.UserMetadataEntityCompanion(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (userId.present) {
map['user_id'] = i0.Variable<i2.Uint8List>(userId.value);
}
if (preferences.present) {
map['preferences'] = i0.Variable<String>(i1
.$UserMetadataEntityTable.$converterpreferences
.toSql(preferences.value));
}
return map;
}
@override
String toString() {
return (StringBuffer('UserMetadataEntityCompanion(')
..write('userId: $userId, ')
..write('preferences: $preferences')
..write(')'))
.toString();
}
}

View File

@@ -1,8 +1,15 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:isar/isar.dart';
import 'db.repository.drift.dart';
// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone
// ref: isar/isar_common.dart
const Symbol _kzoneTxn = #zoneTxn;
@@ -17,3 +24,35 @@ class IsarDatabaseRepository implements IDatabaseRepository {
Future<T> transaction<T>(Future<T> Function() callback) =>
Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback();
}
@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity])
class Drift extends $Drift implements IDatabaseRepository {
Drift([QueryExecutor? executor])
: super(
executor ??
driftDatabase(
name: 'immich',
native: const DriftNativeOptions(shareAcrossIsolates: true),
),
);
@override
int get schemaVersion => 1;
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
await customStatement('PRAGMA journal_mode = WAL');
await customStatement('PRAGMA foreign_keys = ON');
},
);
}
class DriftDatabaseRepository implements IDatabaseRepository {
final Drift _db;
const DriftDatabaseRepository(this._db);
@override
Future<T> transaction<T>(Future<T> Function() callback) =>
_db.transaction(callback);
}

View File

@@ -0,0 +1,67 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i2;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i3;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
$DriftManager get managers => $DriftManager(this);
late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this);
late final i2.$UserMetadataEntityTable userMetadataEntity =
i2.$UserMetadataEntityTable(this);
late final i3.$PartnerEntityTable partnerEntity =
i3.$PartnerEntityTable(this);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@override
List<i0.DatabaseSchemaEntity> get allSchemaEntities =>
[userEntity, userMetadataEntity, partnerEntity];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
const i0.StreamQueryUpdateRules(
[
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('user_metadata_entity',
kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete),
],
),
],
);
@override
i0.DriftDatabaseOptions get options =>
const i0.DriftDatabaseOptions(storeDateTimeAsText: true);
}
class $DriftManager {
final $Drift _db;
$DriftManager(this._db);
i1.$$UserEntityTableTableManager get userEntity =>
i1.$$UserEntityTableTableManager(_db, _db.userEntity);
i2.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i3.$$PartnerEntityTableTableManager get partnerEntity =>
i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
}

View File

@@ -0,0 +1,9 @@
import 'package:drift/drift.dart';
mixin DriftDefaultsMixin on Table {
@override
bool get isStrict => true;
@override
bool get withoutRowId => true;
}

View File

@@ -1,6 +1,7 @@
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:openapi/api.dart';
// TODO: Move to repository once all classes are refactored
abstract final class ExifDtoConverter {
static ExifInfo fromDto(ExifResponseDto dto) {
return ExifInfo(

View File

@@ -1,6 +1,8 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:openapi/api.dart';
// TODO: Move to repository once all classes are refactored
abstract final class UserConverter {
/// Base user dto used where the complete user object is not required
static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto(

View File

@@ -71,8 +71,13 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
Future<List<Asset>> getAllByRemoteId(
Iterable<String> ids, {
AssetState? state,
}) =>
_getAllByRemoteIdImpl(ids, state).findAll();
}) async {
if (ids.isEmpty) {
return [];
}
return _getAllByRemoteIdImpl(ids, state).findAll();
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> _getAllByRemoteIdImpl(
Iterable<String> ids,

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/api.service.dart';

View File

@@ -14,3 +14,6 @@ create_splash:
build_release_android:
flutter build appbundle
migrations:
dart run drift_dev make-migrations

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.131.2
- API version: 1.131.3
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -206,6 +206,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@@ -382,6 +390,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.2"
drift:
dependency: "direct main"
description:
name: drift
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
url: "https://pub.dev"
source: hosted
version: "2.26.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
url: "https://pub.dev"
source: hosted
version: "2.26.0"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
url: "https://pub.dev"
source: hosted
version: "0.2.4"
dynamic_color:
dependency: "direct main"
description:
@@ -1288,6 +1320,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
riverpod:
dependency: transitive
description:
@@ -1549,6 +1589,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
url: "https://pub.dev"
source: hosted
version: "2.7.5"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
url: "https://pub.dev"
source: hosted
version: "0.5.31"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
url: "https://pub.dev"
source: hosted
version: "0.41.0"
stack_trace:
dependency: transitive
description:

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.131.2+192
version: 1.131.3+193
environment:
sdk: '>=3.3.0 <4.0.0'
@@ -73,6 +73,9 @@ dependencies:
isar_flutter_libs: # contains Isar Core
version: *isar_version
hosted: https://pub.isar-community.dev/
# DB
drift: ^2.23.1
drift_flutter: ^0.2.4
dependency_overrides:
analyzer: ^6.0.0
@@ -99,6 +102,8 @@ dev_dependencies:
immich_mobile_immich_lint:
path: './immich_lint'
fake_async: ^1.3.1
# Drift generator
drift_dev: ^2.23.1
flutter:
uses-material-design: true

View File

@@ -1,4 +1,5 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
abstract final class UserStub {
const UserStub._();

View File

@@ -7656,7 +7656,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.131.2",
"version": "1.131.3",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.131.2",
"version": "1.131.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.131.2",
"version": "1.131.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.131.2",
"version": "1.131.3",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.131.2
* 1.131.3
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.131.2",
"version": "1.131.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.131.2",
"version": "1.131.3",
"hasInstallScript": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.131.2",
"version": "1.131.3",
"description": "",
"author": "",
"private": true,

View File

@@ -12,7 +12,7 @@ import { MoveRepository } from 'src/repositories/move.repository';
import { PersonRepository } from 'src/repositories/person.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { getAssetFiles } from 'src/utils/asset.util';
import { getAssetFile } from 'src/utils/asset.util';
import { getConfig } from 'src/utils/config';
export interface MoveRequest {
@@ -117,8 +117,7 @@ export class StorageCore {
async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
const { id: entityId, files } = asset;
const { thumbnailFile, previewFile } = getAssetFiles(files);
const oldFile = pathType === AssetPathType.PREVIEW ? previewFile : thumbnailFile;
const oldFile = getAssetFile(files, pathType);
return this.moveFile({
entityId,
pathType,

View File

@@ -136,6 +136,7 @@ with
"asset_faces"
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
inner join "face_search" on "face_search"."faceId" = "asset_faces"."id"
left join "person" on "person"."id" = "asset_faces"."personId"
where
"assets"."ownerId" = any ($2::uuid[])
and "assets"."deletedAt" is null

View File

@@ -32,47 +32,7 @@ where
"asset_stack"."ownerId" = $1
-- StackRepository.delete
select
*,
(
select
coalesce(json_agg(agg), '[]')
from
(
select
"assets".*,
(
select
coalesce(json_agg(agg), '[]')
from
(
select
"tags".*
from
"tags"
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
where
"tag_asset"."assetsId" = "assets"."id"
) as agg
) as "tags",
to_json("exifInfo") as "exifInfo"
from
"assets"
inner join lateral (
select
"exif".*
from
"exif"
where
"exif"."assetId" = "assets"."id"
) as "exifInfo" on true
where
"assets"."deletedAt" is null
and "assets"."stackId" = "asset_stack"."id"
) as agg
) as "assets"
from
"asset_stack"
delete from "asset_stack"
where
"id" = $1::uuid

View File

@@ -163,6 +163,7 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
hasPerson?: boolean;
numResults: number;
maxDistance: number;
minBirthDate?: Date;
}
export interface AssetDuplicateSearch {
@@ -338,7 +339,7 @@ export class SearchRepository {
},
],
})
searchFaces({ userIds, embedding, numResults, maxDistance, hasPerson }: FaceEmbeddingSearch) {
searchFaces({ userIds, embedding, numResults, maxDistance, hasPerson, minBirthDate }: FaceEmbeddingSearch) {
if (!isValidInteger(numResults, { min: 1, max: 1000 })) {
throw new Error(`Invalid value for 'numResults': ${numResults}`);
}
@@ -354,9 +355,13 @@ export class SearchRepository {
])
.innerJoin('assets', 'assets.id', 'asset_faces.assetId')
.innerJoin('face_search', 'face_search.faceId', 'asset_faces.id')
.leftJoin('person', 'person.id', 'asset_faces.personId')
.where('assets.ownerId', '=', anyUuid(userIds))
.where('assets.deletedAt', 'is', null)
.$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null))
.$if(!!minBirthDate, (qb) =>
qb.where((eb) => eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)])),
)
.orderBy(sql`face_search.embedding <=> ${embedding}`)
.limit(numResults),
)

View File

@@ -122,38 +122,11 @@ export class StackRepository {
@GenerateSql({ params: [DummyValue.UUID] })
async delete(id: string): Promise<void> {
const stack = await this.getById(id);
if (!stack) {
return;
}
const assetIds = stack.assets.map(({ id }) => id);
await this.db.deleteFrom('asset_stack').where('id', '=', asUuid(id)).execute();
await this.db
.updateTable('assets')
.set({ stackId: null, updatedAt: new Date() })
.where('id', 'in', assetIds)
.execute();
}
async deleteAll(ids: string[]): Promise<void> {
const assetIds = [];
for (const id of ids) {
const stack = await this.getById(id);
if (!stack) {
continue;
}
assetIds.push(...stack.assets.map(({ id }) => id));
}
await this.db
.updateTable('assets')
.set({ updatedAt: new Date(), stackId: null })
.where('id', 'in', assetIds)
.where('stackId', 'in', ids)
.execute();
await this.db.deleteFrom('asset_stack').where('id', 'in', ids).execute();
}
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity> {

View File

@@ -584,7 +584,7 @@ describe(AssetMediaService.name, () => {
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
).resolves.toEqual(
new ImmichFileResponse({
path: assetStub.image.files[0].path,
path: '/uploads/user-id/thumbs/path.jpg',
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
contentType: 'image/jpeg',
fileName: 'asset-id_preview.jpg',
@@ -599,7 +599,7 @@ describe(AssetMediaService.name, () => {
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
).resolves.toEqual(
new ImmichFileResponse({
path: assetStub.image.files[1].path,
path: '/uploads/user-id/webp/path.ext',
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
contentType: 'application/octet-stream',
fileName: 'asset-id_thumbnail.ext',

View File

@@ -578,6 +578,7 @@ describe(AssetService.name, () => {
files: [
'/uploads/user-id/webp/path.ext',
'/uploads/user-id/thumbs/path.jpg',
'/uploads/user-id/fullsize/path.webp',
assetWithFace.encodedVideoPath,
assetWithFace.sidecarPath,
assetWithFace.originalPath,
@@ -637,7 +638,14 @@ describe(AssetService.name, () => {
{
name: JobName.DELETE_FILES,
data: {
files: [undefined, undefined, undefined, undefined, 'fake_path/asset_1.jpeg'],
files: [
'/uploads/user-id/webp/path.ext',
'/uploads/user-id/thumbs/path.jpg',
'/uploads/user-id/fullsize/path.webp',
undefined,
undefined,
'fake_path/asset_1.jpeg',
],
},
},
],
@@ -658,7 +666,14 @@ describe(AssetService.name, () => {
{
name: JobName.DELETE_FILES,
data: {
files: [undefined, undefined, undefined, undefined, 'fake_path/asset_1.jpeg'],
files: [
'/uploads/user-id/webp/path.ext',
'/uploads/user-id/thumbs/path.jpg',
'/uploads/user-id/fullsize/path.webp',
undefined,
undefined,
'fake_path/asset_1.jpeg',
],
},
},
],

View File

@@ -233,8 +233,8 @@ export class AssetService extends BaseService {
}
}
const { thumbnailFile, previewFile } = getAssetFiles(asset.files);
const files = [thumbnailFile?.path, previewFile?.path, asset.encodedVideoPath];
const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(asset.files);
const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath];
if (deleteOnDisk) {
files.push(asset.sidecarPath, asset.originalPath);

View File

@@ -136,8 +136,14 @@ export class AuditService extends BaseService {
for await (const assets of pagination) {
assetCount += assets.length;
for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) {
const { previewFile, thumbnailFile } = getAssetFiles(files);
for (const file of [originalPath, previewFile?.path, encodedVideoPath, thumbnailFile?.path]) {
const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(files);
for (const file of [
originalPath,
fullsizeFile?.path,
previewFile?.path,
encodedVideoPath,
thumbnailFile?.path,
]) {
track(file);
}

View File

@@ -5,12 +5,12 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { JobName, JobStatus, QueueName } from 'src/enum';
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
import { WithoutProperty } from 'src/repositories/asset.repository';
import { AssetDuplicateResult } from 'src/repositories/search.repository';
import { BaseService } from 'src/services/base.service';
import { JobOf } from 'src/types';
import { getAssetFiles } from 'src/utils/asset.util';
import { getAssetFile } from 'src/utils/asset.util';
import { isDuplicateDetectionEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
@@ -69,7 +69,7 @@ export class DuplicateService extends BaseService {
return JobStatus.SKIPPED;
}
const { previewFile } = getAssetFiles(asset.files);
const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW);
if (!previewFile) {
this.logger.warn(`Asset ${id} is missing preview image`);
return JobStatus.FAILED;

View File

@@ -234,6 +234,24 @@ describe(MediaService.name, () => {
});
await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: assetStub.image.id,
pathType: AssetPathType.FULLSIZE,
oldPath: '/uploads/user-id/fullsize/path.webp',
newPath: 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg',
});
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: assetStub.image.id,
pathType: AssetPathType.PREVIEW,
oldPath: '/uploads/user-id/thumbs/path.jpg',
newPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
});
expect(mocks.move.create).toHaveBeenCalledWith({
entityId: assetStub.image.id,
pathType: AssetPathType.THUMBNAIL,
oldPath: '/uploads/user-id/webp/path.ext',
newPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
});
expect(mocks.move.create).toHaveBeenCalledTimes(3);
});
});

View File

@@ -140,7 +140,7 @@ export class MediaService extends BaseService {
return JobStatus.FAILED;
}
await this.storageCore.moveAssetImage(asset, AssetPathType.FULLSIZE, ImageFormat.JPEG);
await this.storageCore.moveAssetImage(asset, AssetPathType.FULLSIZE, image.fullsize.format);
await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format);
await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
await this.storageCore.moveAssetVideo(asset);

View File

@@ -1,5 +1,6 @@
import { BinaryField, ExifDateTime } from 'exiftool-vendored';
import { randomBytes } from 'node:crypto';
import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises';
import { defaults } from 'src/config';
import { AssetEntity } from 'src/entities/asset.entity';
@@ -21,14 +22,8 @@ describe(MetadataService.name, () => {
let mocks: ServiceMocks;
const mockReadTags = (exifData?: Partial<ImmichTags>, sidecarData?: Partial<ImmichTags>) => {
exifData = {
FileSize: '123456',
FileCreateDate: '2024-01-01T00:00:00.000Z',
FileModifyDate: '2024-01-01T00:00:00.000Z',
...exifData,
};
mocks.metadata.readTags.mockReset();
mocks.metadata.readTags.mockResolvedValueOnce(exifData);
mocks.metadata.readTags.mockResolvedValueOnce(exifData ?? {});
mocks.metadata.readTags.mockResolvedValueOnce(sidecarData ?? {});
};
@@ -114,6 +109,17 @@ describe(MetadataService.name, () => {
});
describe('handleMetadataExtraction', () => {
beforeEach(() => {
const time = new Date('2022-01-01T00:00:00.000Z');
const timeMs = time.valueOf();
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: time,
mtimeMs: timeMs,
birthtimeMs: timeMs,
} as Stats);
});
it('should handle an asset that could not be found', async () => {
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
@@ -145,10 +151,13 @@ describe(MetadataService.name, () => {
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
mockReadTags({
FileCreateDate: fileCreatedAt.toISOString(),
FileModifyDate: fileModifiedAt.toISOString(),
});
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: fileModifiedAt,
mtimeMs: fileModifiedAt.valueOf(),
birthtimeMs: fileCreatedAt.valueOf(),
} as Stats);
mockReadTags();
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
@@ -168,10 +177,13 @@ describe(MetadataService.name, () => {
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
mockReadTags({
FileCreateDate: fileCreatedAt.toISOString(),
FileModifyDate: fileModifiedAt.toISOString(),
});
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: fileModifiedAt,
mtimeMs: fileModifiedAt.valueOf(),
birthtimeMs: fileCreatedAt.valueOf(),
} as Stats);
mockReadTags();
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
@@ -206,10 +218,14 @@ describe(MetadataService.name, () => {
it('should handle lists of numbers', async () => {
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: assetStub.image.fileModifiedAt,
mtimeMs: assetStub.image.fileModifiedAt.valueOf(),
birthtimeMs: assetStub.image.fileCreatedAt.valueOf(),
} as Stats);
mockReadTags({
ISO: [160],
FileCreateDate: assetStub.image.fileCreatedAt.toISOString(),
FileModifyDate: assetStub.image.fileModifiedAt.toISOString(),
});
await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -228,11 +244,15 @@ describe(MetadataService.name, () => {
mocks.asset.getByIds.mockResolvedValue([assetStub.withLocation]);
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: assetStub.withLocation.fileModifiedAt,
mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(),
birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(),
} as Stats);
mockReadTags({
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
FileCreateDate: assetStub.withLocation.fileCreatedAt.toISOString(),
FileModifyDate: assetStub.withLocation.fileModifiedAt.toISOString(),
});
await sut.handleMetadataExtraction({ id: assetStub.image.id });
@@ -475,6 +495,12 @@ describe(MetadataService.name, () => {
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(),
birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(),
} as Stats);
mockReadTags({
Directory: 'foo/bar/',
MotionPhoto: 1,
@@ -483,8 +509,6 @@ describe(MetadataService.name, () => {
// instead of the EmbeddedVideoFile, since HEIC MotionPhotos include both
EmbeddedVideoFile: new BinaryField(0, ''),
EmbeddedVideoType: 'MotionPhoto_Data',
FileCreateDate: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.toISOString(),
FileModifyDate: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.toISOString(),
});
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
@@ -525,14 +549,18 @@ describe(MetadataService.name, () => {
});
it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => {
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(),
birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(),
} as Stats);
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
mockReadTags({
Directory: 'foo/bar/',
EmbeddedVideoFile: new BinaryField(0, ''),
EmbeddedVideoType: 'MotionPhoto_Data',
MotionPhoto: 1,
FileCreateDate: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.toISOString(),
FileModifyDate: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.toISOString(),
});
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
@@ -574,13 +602,17 @@ describe(MetadataService.name, () => {
it('should extract the motion photo video from the XMP directory entry ', async () => {
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(),
birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(),
} as Stats);
mockReadTags({
Directory: 'foo/bar/',
MotionPhoto: 1,
MicroVideo: 1,
MicroVideoOffset: 1,
FileCreateDate: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.toISOString(),
FileModifyDate: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.toISOString(),
});
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset);

View File

@@ -1,9 +1,10 @@
import { Injectable } from '@nestjs/common';
import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored';
import { ContainerDirectoryItem, Maybe, Tags } from 'exiftool-vendored';
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
import { Insertable } from 'kysely';
import _ from 'lodash';
import { Duration } from 'luxon';
import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises';
import path from 'node:path';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
@@ -77,6 +78,11 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
type ImmichTagsWithFaces = ImmichTags & { RegionInfo: NonNullable<ImmichTags['RegionInfo']> };
type Dates = {
dateTimeOriginal: Date;
localDateTime: Date;
};
@Injectable()
export class MetadataService extends BaseService {
@OnEvent({ name: 'app.bootstrap', workers: [ImmichWorker.MICROSERVICES] })
@@ -111,6 +117,7 @@ export class MetadataService extends BaseService {
this.logger.log(`Initialized local reverse geocoder`);
} catch (error: Error | any) {
this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack);
throw new Error(`Metadata service init failed`);
}
}
@@ -171,18 +178,13 @@ export class MetadataService extends BaseService {
return JobStatus.FAILED;
}
const exifTags = await this.getExifTags(asset);
if (!exifTags.FileCreateDate || !exifTags.FileModifyDate || exifTags.FileSize === undefined) {
this.logger.warn(`Missing file creation or modification date for asset ${asset.id}: ${asset.originalPath}`);
const stat = await this.storageRepository.stat(asset.originalPath);
exifTags.FileCreateDate = stat.ctime.toISOString();
exifTags.FileModifyDate = stat.mtime.toISOString();
exifTags.FileSize = stat.size.toString();
}
const [exifTags, stats] = await Promise.all([
this.getExifTags(asset),
this.storageRepository.stat(asset.originalPath),
]);
this.logger.verbose('Exif Tags', exifTags);
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
const dates = this.getDates(asset, exifTags, stats);
const { width, height } = this.getImageDimensions(exifTags);
let geo: ReverseGeocodeResult, latitude: number | null, longitude: number | null;
@@ -200,9 +202,9 @@ export class MetadataService extends BaseService {
assetId: asset.id,
// dates
dateTimeOriginal,
modifyDate,
timeZone,
dateTimeOriginal: dates.dateTimeOriginal,
modifyDate: stats.mtime,
timeZone: dates.timeZone,
// gps
latitude,
@@ -212,7 +214,7 @@ export class MetadataService extends BaseService {
city: geo.city,
// image/file
fileSizeInByte: Number.parseInt(exifTags.FileSize!),
fileSizeInByte: stats.size,
exifImageHeight: validate(height),
exifImageWidth: validate(width),
orientation: validate(exifTags.Orientation)?.toString() ?? null,
@@ -245,15 +247,15 @@ export class MetadataService extends BaseService {
this.assetRepository.update({
id: asset.id,
duration: exifTags.Duration?.toString() ?? null,
localDateTime,
fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
fileModifiedAt: exifData.modifyDate ?? undefined,
localDateTime: dates.localDateTime,
fileCreatedAt: dates.dateTimeOriginal ?? undefined,
fileModifiedAt: stats.mtime,
}),
this.applyTagList(asset, exifTags),
];
if (this.isMotionPhoto(asset, exifTags)) {
promises.push(this.applyMotionPhotos(asset, exifTags, exifData.fileSizeInByte!));
promises.push(this.applyMotionPhotos(asset, exifTags, dates, stats));
}
if (isFaceImportEnabled(metadata) && this.hasTaggedFaces(exifTags)) {
@@ -432,7 +434,7 @@ export class MetadataService extends BaseService {
return asset.type === AssetType.IMAGE && !!(tags.MotionPhoto || tags.MicroVideo);
}
private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags, fileSize: number) {
private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags, dates: Dates, stats: Stats) {
const isMotionPhoto = tags.MotionPhoto;
const isMicroVideo = tags.MicroVideo;
const videoOffset = tags.MicroVideoOffset;
@@ -466,7 +468,7 @@ export class MetadataService extends BaseService {
this.logger.debug(`Starting motion photo video extraction for asset ${asset.id}: ${asset.originalPath}`);
try {
const position = fileSize - length - padding;
const position = stats.size - length - padding;
let video: Buffer;
// Samsung MotionPhoto video extraction
// HEIC-encoded
@@ -505,13 +507,12 @@ export class MetadataService extends BaseService {
}
} else {
const motionAssetId = this.cryptoRepository.randomUUID();
const dates = this.getDates(asset, tags);
motionAsset = await this.assetRepository.create({
id: motionAssetId,
libraryId: asset.libraryId,
type: AssetType.VIDEO,
fileCreatedAt: dates.dateTimeOriginal,
fileModifiedAt: dates.modifyDate,
fileModifiedAt: stats.mtime,
localDateTime: dates.localDateTime,
checksum,
ownerId: asset.ownerId,
@@ -634,7 +635,7 @@ export class MetadataService extends BaseService {
}
}
private getDates(asset: AssetEntity, exifTags: ImmichTags) {
private getDates(asset: AssetEntity, exifTags: ImmichTags, stats: Stats) {
const dateTime = firstDateTime(exifTags as Maybe<Tags>, EXIF_DATE_TAGS);
this.logger.verbose(`Date and time is ${dateTime} for asset ${asset.id}: ${asset.originalPath}`);
@@ -654,17 +655,16 @@ export class MetadataService extends BaseService {
this.logger.debug(`No timezone information found for asset ${asset.id}: ${asset.originalPath}`);
}
const modifyDate = this.toDate(exifTags.FileModifyDate!);
let dateTimeOriginal = dateTime?.toDate();
let localDateTime = dateTime?.toDateTime().setZone('UTC', { keepLocalTime: true }).toJSDate();
if (!localDateTime || !dateTimeOriginal) {
const fileCreatedAt = this.toDate(exifTags.FileCreateDate!);
const earliestDate = this.earliestDate(fileCreatedAt, modifyDate);
// FileCreateDate is not available on linux, likely because exiftool hasn't integrated the statx syscall yet
// birthtime is not available in Docker on macOS, so it appears as 0
const earliestDate = stats.birthtimeMs ? new Date(Math.min(stats.mtimeMs, stats.birthtimeMs)) : stats.mtime;
this.logger.debug(
`No exif date time found, falling back on ${earliestDate.toISOString()}, earliest of file creation and modification for assset ${asset.id}: ${asset.originalPath}`,
`No exif date time found, falling back on ${earliestDate.toISOString()}, earliest of file creation and modification for asset ${asset.id}: ${asset.originalPath}`,
);
dateTimeOriginal = earliestDate;
localDateTime = earliestDate;
dateTimeOriginal = localDateTime = earliestDate;
}
this.logger.verbose(
@@ -675,18 +675,9 @@ export class MetadataService extends BaseService {
dateTimeOriginal,
timeZone,
localDateTime,
modifyDate,
};
}
private toDate(date: string | ExifDateTime): Date {
return typeof date === 'string' ? new Date(date) : date.toDate();
}
private earliestDate(a: Date, b: Date) {
return new Date(Math.min(a.valueOf(), b.valueOf()));
}
private hasGeo(tags: ImmichTags): tags is ImmichTags & { GPSLatitude: number; GPSLongitude: number } {
return (
tags.GPSLatitude !== undefined &&

View File

@@ -2,12 +2,12 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { OnEvent, OnJob } from 'src/decorators';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { JobName, JobStatus, QueueName } from 'src/enum';
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
import { ArgOf } from 'src/repositories/event.repository';
import { EmailTemplate } from 'src/repositories/notification.repository';
import { BaseService } from 'src/services/base.service';
import { EmailImageAttachment, IEntityJob, INotifyAlbumUpdateJob, JobItem, JobOf } from 'src/types';
import { getAssetFiles } from 'src/utils/asset.util';
import { getAssetFile } from 'src/utils/asset.util';
import { getFilenameExtension } from 'src/utils/file';
import { getExternalDomain } from 'src/utils/misc';
import { isEqualObject } from 'src/utils/object';
@@ -398,7 +398,11 @@ export class NotificationService extends BaseService {
}
const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId, { files: true });
const { thumbnailFile } = getAssetFiles(albumThumbnail?.files);
if (!albumThumbnail) {
return;
}
const thumbnailFile = getAssetFile(albumThumbnail.files, AssetFileType.THUMBNAIL);
if (!thumbnailFile) {
return;
}

View File

@@ -896,6 +896,66 @@ describe(PersonService.name, () => {
});
});
it('should match existing person if their birth date is unknown', async () => {
if (!faceStub.primaryFace1.person) {
throw new Error('faceStub.primaryFace1.person is null');
}
const faces = [
{ ...faceStub.noPerson1, distance: 0 },
{ ...faceStub.primaryFace1, distance: 0.2 },
{ ...faceStub.withBirthDate, distance: 0.3 },
] as FaceSearchResult[];
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
mocks.search.searchFaces.mockResolvedValue(faces);
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person);
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
expect(mocks.person.create).not.toHaveBeenCalled();
expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1);
expect(mocks.person.reassignFaces).toHaveBeenCalledWith({
faceIds: expect.arrayContaining([faceStub.noPerson1.id]),
newPersonId: faceStub.primaryFace1.person.id,
});
expect(mocks.person.reassignFaces).toHaveBeenCalledWith({
faceIds: expect.not.arrayContaining([faceStub.face1.id]),
newPersonId: faceStub.primaryFace1.person.id,
});
});
it('should match existing person if their birth date is before file creation', async () => {
if (!faceStub.primaryFace1.person) {
throw new Error('faceStub.primaryFace1.person is null');
}
const faces = [
{ ...faceStub.noPerson1, distance: 0 },
{ ...faceStub.withBirthDate, distance: 0.2 },
{ ...faceStub.primaryFace1, distance: 0.3 },
] as FaceSearchResult[];
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
mocks.search.searchFaces.mockResolvedValue(faces);
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person);
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
expect(mocks.person.create).not.toHaveBeenCalled();
expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1);
expect(mocks.person.reassignFaces).toHaveBeenCalledWith({
faceIds: expect.arrayContaining([faceStub.noPerson1.id]),
newPersonId: faceStub.withBirthDate.person?.id,
});
expect(mocks.person.reassignFaces).toHaveBeenCalledWith({
faceIds: expect.not.arrayContaining([faceStub.face1.id]),
newPersonId: faceStub.withBirthDate.person?.id,
});
});
it('should create a new person if the face is a core point with no person', async () => {
const faces = [
{ ...faceStub.noPerson1, distance: 0 },

View File

@@ -26,6 +26,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { FaceSearchEntity } from 'src/entities/face-search.entity';
import { PersonEntity } from 'src/entities/person.entity';
import {
AssetFileType,
AssetType,
CacheControl,
ImageFormat,
@@ -42,7 +43,7 @@ import { BoundingBox } from 'src/repositories/machine-learning.repository';
import { UpdateFacesData } from 'src/repositories/person.repository';
import { BaseService } from 'src/services/base.service';
import { CropOptions, ImageDimensions, InputDimensions, JobItem, JobOf } from 'src/types';
import { getAssetFiles } from 'src/utils/asset.util';
import { getAssetFile } from 'src/utils/asset.util';
import { ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types';
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
@@ -300,7 +301,7 @@ export class PersonService extends BaseService {
const relations = { exifInfo: true, faces: { person: false, withDeleted: true }, files: true };
const [asset] = await this.assetRepository.getByIds([id], relations);
const { previewFile } = getAssetFiles(asset.files);
const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW);
if (!asset || !previewFile) {
return JobStatus.FAILED;
}
@@ -482,6 +483,7 @@ export class PersonService extends BaseService {
embedding: face.faceSearch.embedding,
maxDistance: machineLearning.facialRecognition.maxDistance,
numResults: machineLearning.facialRecognition.minFaces,
minBirthDate: face.asset.fileCreatedAt,
});
// `matches` also includes the face itself
@@ -507,6 +509,7 @@ export class PersonService extends BaseService {
maxDistance: machineLearning.facialRecognition.maxDistance,
numResults: 1,
hasPerson: true,
minBirthDate: face.asset.fileCreatedAt,
});
if (matchWithPerson.length > 0) {
@@ -674,7 +677,7 @@ export class PersonService extends BaseService {
throw new Error(`Asset ${asset.id} dimensions are unknown`);
}
const { previewFile } = getAssetFiles(asset.files);
const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW);
if (!previewFile) {
throw new Error(`Asset ${asset.id} has no preview path`);
}

View File

@@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { OnEvent, OnJob } from 'src/decorators';
import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
import { AssetFileType, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
import { WithoutProperty } from 'src/repositories/asset.repository';
import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { JobOf } from 'src/types';
import { getAssetFiles } from 'src/utils/asset.util';
import { getAssetFile } from 'src/utils/asset.util';
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
@@ -116,7 +116,7 @@ export class SmartInfoService extends BaseService {
return JobStatus.SKIPPED;
}
const { previewFile } = getAssetFiles(asset.files);
const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW);
if (!previewFile) {
return JobStatus.FAILED;
}

View File

@@ -1,5 +1,5 @@
import { BadRequestException } from '@nestjs/common';
import { StorageCore } from 'src/cores/storage.core';
import { GeneratedImageType, StorageCore } from 'src/cores/storage.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { UploadFieldName } from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -13,14 +13,14 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
import { IBulkAsset, ImmichFile, UploadFile } from 'src/types';
import { checkAccess } from 'src/utils/access';
const getFileByType = (files: AssetFileEntity[] | undefined, type: AssetFileType) => {
export const getAssetFile = (files: AssetFileEntity[], type: AssetFileType | GeneratedImageType) => {
return (files || []).find((file) => file.type === type);
};
export const getAssetFiles = (files?: AssetFileEntity[]) => ({
fullsizeFile: getFileByType(files, AssetFileType.FULLSIZE),
previewFile: getFileByType(files, AssetFileType.PREVIEW),
thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL),
export const getAssetFiles = (files: AssetFileEntity[]) => ({
fullsizeFile: getAssetFile(files, AssetFileType.FULLSIZE),
previewFile: getAssetFile(files, AssetFileType.PREVIEW),
thumbnailFile: getAssetFile(files, AssetFileType.THUMBNAIL),
});
export const addAssets = async (

View File

@@ -128,6 +128,8 @@ export class BaseConfig implements VideoCodecSWConfig {
'-fps_mode passthrough',
// explicitly selects the video stream instead of leaving it up to FFmpeg
`-map 0:${videoStream.index}`,
// Strip metadata like capture date, camera, and GPS
'-map_metadata -1',
];
if (audioStream) {

View File

@@ -1,9 +1,9 @@
import { Insertable, Kysely } from 'kysely';
import { randomBytes } from 'node:crypto';
import { Writable } from 'node:stream';
import { Assets, DB, Partners, Sessions } from 'src/db';
import { AssetFaces, Assets, DB, Person as DbPerson, FaceSearch, Partners, Sessions } from 'src/db';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetType } from 'src/enum';
import { AssetType, SourceType } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
@@ -37,7 +37,7 @@ import { VersionHistoryRepository } from 'src/repositories/version-history.repos
import { ViewRepository } from 'src/repositories/view-repository';
import { UserTable } from 'src/schema/tables/user.table';
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
import { newUuid } from 'test/small.factory';
import { newDate, newEmbedding, newUuid } from 'test/small.factory';
import { automock } from 'test/utils';
class CustomWritable extends Writable {
@@ -61,12 +61,18 @@ type Asset = Partial<Insertable<Assets>>;
type User = Partial<Insertable<UserTable>>;
type Session = Omit<Insertable<Sessions>, 'token'> & { token?: string };
type Partner = Insertable<Partners>;
type AssetFace = Partial<Insertable<AssetFaces>>;
type Person = Partial<Insertable<DbPerson>>;
type Face = Partial<Insertable<FaceSearch>>;
export class TestFactory {
private assets: Asset[] = [];
private sessions: Session[] = [];
private users: User[] = [];
private partners: Partner[] = [];
private assetFaces: AssetFace[] = [];
private persons: Person[] = [];
private faces: Face[] = [];
private constructor(private context: TestContext) {}
@@ -141,6 +147,53 @@ export class TestFactory {
};
}
static assetFace(assetFace: AssetFace) {
const defaults = {
assetId: assetFace.assetId || newUuid(),
boundingBoxX1: assetFace.boundingBoxX1 || 0,
boundingBoxX2: assetFace.boundingBoxX2 || 1,
boundingBoxY1: assetFace.boundingBoxY1 || 0,
boundingBoxY2: assetFace.boundingBoxY2 || 1,
deletedAt: assetFace.deletedAt || null,
id: assetFace.id || newUuid(),
imageHeight: assetFace.imageHeight || 10,
imageWidth: assetFace.imageWidth || 10,
personId: assetFace.personId || null,
sourceType: assetFace.sourceType || SourceType.MACHINE_LEARNING,
};
return { ...defaults, ...assetFace };
}
static person(person: Person) {
const defaults = {
birthDate: person.birthDate || null,
color: person.color || null,
createdAt: person.createdAt || newDate(),
faceAssetId: person.faceAssetId || null,
id: person.id || newUuid(),
isFavorite: person.isFavorite || false,
isHidden: person.isHidden || false,
name: person.name || 'Test Name',
ownerId: person.ownerId || newUuid(),
thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg',
updatedAt: person.updatedAt || newDate(),
updateId: person.updateId || newUuid(),
};
return { ...defaults, ...person };
}
static face(face: Face) {
const defaults = {
faceId: face.faceId || newUuid(),
embedding: face.embedding || newEmbedding(),
};
return {
...defaults,
...face,
};
}
withAsset(asset: Asset) {
this.assets.push(asset);
return this;
@@ -161,6 +214,21 @@ export class TestFactory {
return this;
}
withAssetFace(assetFace: AssetFace) {
this.assetFaces.push(assetFace);
return this;
}
withPerson(person: Person) {
this.persons.push(person);
return this;
}
withFaces(face: Face) {
this.faces.push(face);
return this;
}
async create() {
for (const user of this.users) {
await this.context.createUser(user);
@@ -178,6 +246,16 @@ export class TestFactory {
await this.context.createAsset(asset);
}
for (const person of this.persons) {
await this.context.createPerson(person);
}
await this.context.refreshFaces(
this.assetFaces,
[],
this.faces.map((f) => TestFactory.face(f)),
);
return this.context;
}
}
@@ -276,4 +354,16 @@ export class TestContext {
createSession(session: Session) {
return this.session.create(TestFactory.session(session));
}
createPerson(person: Person) {
return this.person.create(TestFactory.person(person));
}
refreshFaces(facesToAdd: AssetFace[], faceIdsToRemove: string[], embeddingsToAdd?: Insertable<FaceSearch>[]) {
return this.person.refreshFaces(
facesToAdd.map((f) => TestFactory.assetFace(f)),
faceIdsToRemove,
embeddingsToAdd,
);
}
}

View File

@@ -26,7 +26,16 @@ const thumbnailFile: AssetFileEntity = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
};
const files: AssetFileEntity[] = [previewFile, thumbnailFile];
const fullsizeFile: AssetFileEntity = {
id: 'file-3',
assetId: 'asset-id',
type: AssetFileType.FULLSIZE,
path: '/uploads/user-id/fullsize/path.webp',
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
};
const files: AssetFileEntity[] = [fullsizeFile, previewFile, thumbnailFile];
export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => {
return {
@@ -553,6 +562,7 @@ export const assetStub = {
fileSizeInByte: 25_000,
timeZone: `America/New_York`,
},
files,
} as AssetEntity),
livePhotoWithOriginalFileName: Object.freeze({

View File

@@ -164,4 +164,19 @@ export const faceStub = {
sourceType: SourceType.EXIF,
deletedAt: null,
}),
withBirthDate: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId10',
assetId: assetStub.image.id,
asset: assetStub.image,
personId: personStub.withBirthDate.id,
person: personStub.withBirthDate,
boundingBoxX1: 0,
boundingBoxY1: 0,
boundingBoxX2: 1,
boundingBoxY2: 1,
imageHeight: 1024,
imageWidth: 1024,
sourceType: SourceType.MACHINE_LEARNING,
deletedAt: null,
}),
};

View File

@@ -39,7 +39,12 @@ describe(MetadataService.name, () => {
beforeEach(() => {
({ sut, mocks } = newTestService(MetadataService, { metadata: metadataRepository }));
mocks.storage.stat.mockResolvedValue({ size: 123_456, ctime: new Date(), mtime: new Date() } as Stats);
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: new Date(654_321),
mtimeMs: 654_321,
birthtimeMs: 654_322,
} as Stats);
delete process.env.TZ;
});
@@ -54,8 +59,6 @@ describe(MetadataService.name, () => {
description: 'should handle no time zone information',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00',
FileCreateDate: '2022:01:01 00:00:00',
FileModifyDate: '2022:01:01 00:00:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',
@@ -68,8 +71,6 @@ describe(MetadataService.name, () => {
serverTimeZone: 'America/Los_Angeles',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00',
FileCreateDate: '2022:01:01 00:00:00',
FileModifyDate: '2022:01:01 00:00:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',
@@ -82,8 +83,6 @@ describe(MetadataService.name, () => {
serverTimeZone: 'Europe/Brussels',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00',
FileCreateDate: '2022:01:01 00:00:00',
FileModifyDate: '2022:01:01 00:00:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',
@@ -96,8 +95,6 @@ describe(MetadataService.name, () => {
serverTimeZone: 'Europe/Brussels',
exifData: {
DateTimeOriginal: '2022:06:01 00:00:00',
FileCreateDate: '2022:06:01 00:00:00',
FileModifyDate: '2022:06:01 00:00:00',
},
expected: {
localDateTime: '2022-06-01T00:00:00.000Z',
@@ -109,8 +106,6 @@ describe(MetadataService.name, () => {
description: 'should handle a +13:00 time zone',
exifData: {
DateTimeOriginal: '2022:01:01 00:00:00+13:00',
FileCreateDate: '2022:01:01 00:00:00+13:00',
FileModifyDate: '2022:01:01 00:00:00+13:00',
},
expected: {
localDateTime: '2022-01-01T00:00:00.000Z',

View File

@@ -0,0 +1,201 @@
import { Kysely } from 'kysely';
import { JobStatus, SourceType } from 'src/enum';
import { PersonService } from 'src/services/person.service';
import { TestContext, TestFactory } from 'test/factory';
import { newEmbedding } from 'test/small.factory';
import { getKyselyDB, newTestService } from 'test/utils';
const setup = async (db: Kysely<any>) => {
const context = await TestContext.from(db).create();
const { sut, mocks } = newTestService(PersonService, context);
return { sut, mocks, context };
};
describe.concurrent(PersonService.name, () => {
let sut: PersonService;
let context: TestContext;
beforeAll(async () => {
({ sut, context } = await setup(await getKyselyDB()));
});
describe('handleRecognizeFaces', () => {
it('should skip if face source type is not MACHINE_LEARNING', async () => {
const user = TestFactory.user();
const asset = TestFactory.asset({ ownerId: user.id });
const assetFace = TestFactory.assetFace({ assetId: asset.id, sourceType: SourceType.MANUAL });
const face = TestFactory.face({ faceId: assetFace.id });
await context.getFactory().withUser(user).withAsset(asset).withAssetFace(assetFace).withFaces(face).create();
const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false });
expect(result).toBe(JobStatus.SKIPPED);
const newPersonId = await context.db
.selectFrom('asset_faces')
.select('asset_faces.personId')
.where('asset_faces.id', '=', assetFace.id)
.executeTakeFirst();
expect(newPersonId?.personId).toBeNull();
});
it('should fail if face does not have an embedding', async () => {
const user = TestFactory.user();
const asset = TestFactory.asset({ ownerId: user.id });
const assetFace = TestFactory.assetFace({ assetId: asset.id, sourceType: SourceType.MACHINE_LEARNING });
await context.getFactory().withUser(user).withAsset(asset).withAssetFace(assetFace).create();
const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false });
expect(result).toBe(JobStatus.FAILED);
const newPersonId = await context.db
.selectFrom('asset_faces')
.select('asset_faces.personId')
.where('asset_faces.id', '=', assetFace.id)
.executeTakeFirst();
expect(newPersonId?.personId).toBeNull();
});
it('should skip if face already has a person assigned', async () => {
const user = TestFactory.user();
const asset = TestFactory.asset({ ownerId: user.id });
const person = TestFactory.person({ ownerId: user.id });
const assetFace = TestFactory.assetFace({
assetId: asset.id,
sourceType: SourceType.MACHINE_LEARNING,
personId: person.id,
});
const face = TestFactory.face({ faceId: assetFace.id });
await context
.getFactory()
.withUser(user)
.withAsset(asset)
.withPerson(person)
.withAssetFace(assetFace)
.withFaces(face)
.create();
const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false });
expect(result).toBe(JobStatus.SKIPPED);
const newPersonId = await context.db
.selectFrom('asset_faces')
.select('asset_faces.personId')
.where('asset_faces.id', '=', assetFace.id)
.executeTakeFirst();
expect(newPersonId?.personId).toEqual(person.id);
});
it('should create a new person if no matches are found', async () => {
const user = TestFactory.user();
const embedding = newEmbedding();
let factory = context.getFactory().withUser(user);
for (let i = 0; i < 3; i++) {
const existingAsset = TestFactory.asset({ ownerId: user.id });
const existingAssetFace = TestFactory.assetFace({
assetId: existingAsset.id,
sourceType: SourceType.MACHINE_LEARNING,
});
const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding });
factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace);
}
const newAsset = TestFactory.asset({ ownerId: user.id });
const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING });
const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding });
await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create();
const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false });
expect(result).toBe(JobStatus.SUCCESS);
const newPersonId = await context.db
.selectFrom('asset_faces')
.select('asset_faces.personId')
.where('asset_faces.id', '=', newAssetFace.id)
.executeTakeFirstOrThrow();
expect(newPersonId.personId).toBeDefined();
});
it('should assign face to an existing person if matches are found', async () => {
const user = TestFactory.user();
const existingPerson = TestFactory.person({ ownerId: user.id });
const embedding = newEmbedding();
let factory = context.getFactory().withUser(user).withPerson(existingPerson);
const assetFaces: string[] = [];
for (let i = 0; i < 3; i++) {
const existingAsset = TestFactory.asset({ ownerId: user.id });
const existingAssetFace = TestFactory.assetFace({
assetId: existingAsset.id,
sourceType: SourceType.MACHINE_LEARNING,
});
assetFaces.push(existingAssetFace.id);
const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding });
factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace);
}
const newAsset = TestFactory.asset({ ownerId: user.id });
const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING });
const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding });
await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create();
await context.person.reassignFaces({ newPersonId: existingPerson.id, faceIds: assetFaces });
const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false });
expect(result).toBe(JobStatus.SUCCESS);
const after = await context.db
.selectFrom('asset_faces')
.select('asset_faces.personId')
.where('asset_faces.id', '=', newAssetFace.id)
.executeTakeFirstOrThrow();
expect(after.personId).toEqual(existingPerson.id);
});
it('should not assign face to an existing person if asset is older than person', async () => {
const user = TestFactory.user();
const assetCreatedAt = new Date('2020-02-23T05:06:29.716Z');
const birthDate = new Date(assetCreatedAt.getTime() + 3600 * 1000 * 365);
const existingPerson = TestFactory.person({ ownerId: user.id, birthDate });
const embedding = newEmbedding();
let factory = context.getFactory().withUser(user).withPerson(existingPerson);
const assetFaces: string[] = [];
for (let i = 0; i < 3; i++) {
const existingAsset = TestFactory.asset({ ownerId: user.id });
const existingAssetFace = TestFactory.assetFace({
assetId: existingAsset.id,
sourceType: SourceType.MACHINE_LEARNING,
});
assetFaces.push(existingAssetFace.id);
const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding });
factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace);
}
const newAsset = TestFactory.asset({ ownerId: user.id, fileCreatedAt: assetCreatedAt });
const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING });
const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding });
await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create();
await context.person.reassignFaces({ newPersonId: existingPerson.id, faceIds: assetFaces });
const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false });
expect(result).toBe(JobStatus.SKIPPED);
const after = await context.db
.selectFrom('asset_faces')
.select('asset_faces.personId')
.where('asset_faces.id', '=', newAssetFace.id)
.executeTakeFirstOrThrow();
expect(after.personId).toBeNull();
});
});
});

View File

@@ -0,0 +1,36 @@
import { PersonRepository } from 'src/repositories/person.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newPersonRepositoryMock = (): Mocked<RepositoryInterface<PersonRepository>> => {
return {
reassignFaces: vitest.fn(),
unassignFaces: vitest.fn(),
delete: vitest.fn(),
deleteFaces: vitest.fn(),
getAllFaces: vitest.fn(),
getAll: vitest.fn(),
getAllForUser: vitest.fn(),
getAllWithoutFaces: vitest.fn(),
getFaces: vitest.fn(),
getFaceById: vitest.fn(),
getFaceByIdWithAssets: vitest.fn(),
reassignFace: vitest.fn(),
getById: vitest.fn(),
getByName: vitest.fn(),
getDistinctNames: vitest.fn(),
getStatistics: vitest.fn(),
getNumberOfPeople: vitest.fn(),
create: vitest.fn(),
createAll: vitest.fn(),
refreshFaces: vitest.fn(),
update: vitest.fn(),
updateAll: vitest.fn(),
getFacesByIds: vitest.fn(),
getRandomFace: vitest.fn(),
getLatestFaceDate: vitest.fn(),
createAssetFace: vitest.fn(),
deleteAssetFace: vitest.fn(),
softDeleteAssetFaces: vitest.fn(),
};
};

View File

@@ -12,6 +12,12 @@ export const newUuids = () =>
export const newDate = () => new Date();
export const newUpdateId = () => 'uuid-v7';
export const newSha1 = () => Buffer.from('this is a fake hash');
export const newEmbedding = () => {
const embedding = Array.from({ length: 512 })
.fill(0)
.map(() => Math.random());
return '[' + embedding + ']';
};
const authFactory = ({ apiKey, ...user }: Partial<AuthUser> & { apiKey?: Partial<AuthApiKey> } = {}) => {
const auth: AuthDto = {

View File

@@ -58,6 +58,7 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
@@ -197,7 +198,7 @@ export const newTestService = <T extends BaseService>(
notification: automock(NotificationRepository, { args: [loggerMock] }),
oauth: automock(OAuthRepository, { args: [loggerMock] }),
partner: automock(PartnerRepository, { strict: false }),
person: automock(PersonRepository, { strict: false }),
person: newPersonRepositoryMock(),
process: automock(ProcessRepository, { args: [loggerMock] }),
search: automock(SearchRepository, { args: [loggerMock], strict: false }),
// eslint-disable-next-line no-sparse-arrays

6
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.131.2",
"version": "1.131.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.131.2",
"version": "1.131.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -81,7 +81,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.131.2",
"version": "1.131.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.131.2",
"version": "1.131.3",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -3,15 +3,16 @@
interface Props {
show: boolean;
active?: boolean;
}
let { show = $bindable() }: Props = $props();
let { show = $bindable(), active = $bindable() }: Props = $props();
</script>
<button type="button" onclick={() => (show = true)}>Open</button>
{#if show}
<div use:focusTrap>
<div use:focusTrap={{ active }}>
<div>
<span>text</span>
<button data-testid="one" type="button" onclick={() => (show = false)}>Close</button>

View File

@@ -12,6 +12,12 @@ describe('focusTrap action', () => {
expect(document.activeElement).toEqual(screen.getByTestId('one'));
});
it('should not set focus if inactive', async () => {
render(FocusTrapTest, { show: true, active: false });
await tick();
expect(document.activeElement).toBe(document.body);
});
it('supports backward focus wrapping', async () => {
render(FocusTrapTest, { show: true });
await tick();

View File

@@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
}
};
document.addEventListener('mousedown', handleClick, true);
document.addEventListener('mousedown', handleClick, false);
node.addEventListener('keydown', handleKey, false);
return {
destroy() {
document.removeEventListener('mousedown', handleClick, true);
document.removeEventListener('mousedown', handleClick, false);
node.removeEventListener('keydown', handleKey, false);
},
};

View File

@@ -1,16 +1,34 @@
import { shortcuts } from '$lib/actions/shortcut';
import { tick } from 'svelte';
const selectors =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
interface Options {
/**
* Set whether the trap is active or not.
*/
active?: boolean;
}
export function focusTrap(container: HTMLElement) {
const selectors =
'button:not([disabled], .hidden), [href]:not(.hidden), input:not([disabled], .hidden), select:not([disabled], .hidden), textarea:not([disabled], .hidden), [tabindex]:not([tabindex="-1"], .hidden)';
export function focusTrap(container: HTMLElement, options?: Options) {
const triggerElement = document.activeElement;
const focusableElement = container.querySelector<HTMLElement>(selectors);
const withDefaults = (options?: Options) => {
return {
active: options?.active ?? true,
};
};
// Use tick() to ensure focus trap works correctly inside <Portal />
void tick().then(() => focusableElement?.focus());
const setInitialFocus = () => {
const focusableElement = container.querySelector<HTMLElement>(selectors);
// Use tick() to ensure focus trap works correctly inside <Portal />
void tick().then(() => focusableElement?.focus());
};
if (withDefaults(options).active) {
setInitialFocus();
}
const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
const focusableElements = container.querySelectorAll<HTMLElement>(selectors);
@@ -27,7 +45,7 @@ export function focusTrap(container: HTMLElement) {
shortcut: { key: 'Tab' },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === lastElement) {
if (document.activeElement === lastElement && withDefaults(options).active) {
event.preventDefault();
firstElement?.focus();
}
@@ -39,7 +57,7 @@ export function focusTrap(container: HTMLElement) {
shortcut: { key: 'Tab', shift: true },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === firstElement) {
if (document.activeElement === firstElement && withDefaults(options).active) {
event.preventDefault();
lastElement?.focus();
}
@@ -48,6 +66,12 @@ export function focusTrap(container: HTMLElement) {
]);
return {
update(newOptions?: Options) {
options = newOptions;
if (withDefaults(options).active) {
setInitialFocus();
}
},
destroy() {
destroyShortcuts?.();
if (triggerElement instanceof HTMLElement) {

View File

@@ -592,29 +592,27 @@
id="stack-slideshow"
class="z-[1002] flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 overflow-x-auto horizontal-scrollbar"
>
<div class="relative w-full whitespace-nowrap transition-all">
<div class="relative w-full whitespace-nowrap">
{#each stackedAssets as stackedAsset (stackedAsset.id)}
<div
class="{stackedAsset.id == asset.id
? '-translate-y-[1px]'
: '-translate-y-0'} inline-block px-1 transition-transform"
class={['inline-block px-1 relative transition-all pb-2']}
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
>
<Thumbnail
class="{stackedAsset.id == asset.id
? 'bg-transparent border-2 border-white'
: 'bg-gray-700/40'} inline-block hover:bg-transparent"
imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
dimmed={stackedAsset.id !== asset.id}
asset={stackedAsset}
onClick={(stackedAsset) => {
asset = stackedAsset;
}}
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
disableMouseOver
readonly
thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
thumbnailSize={stackedAsset.id === asset.id ? 65 : 60}
showStackedIcon={false}
disableLinkMouseOver
/>
{#if stackedAsset.id == asset.id}
{#if stackedAsset.id === asset.id}
<div class="w-full flex place-items-center place-content-center">
<div class="w-2 h-2 bg-white rounded-full flex mt-[2px]"></div>
</div>

View File

@@ -23,6 +23,9 @@ vi.hoisted(() => {
describe('Thumbnail component', () => {
beforeAll(() => {
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
vi.mock('$lib/utils/navigation', () => ({
currentUrlReplaceAssetId: vi.fn(),
}));
});
it('should only contain a single tabbable element (the container)', () => {
@@ -30,7 +33,6 @@ describe('Thumbnail component', () => {
render(Thumbnail, {
asset,
focussed: false,
overrideDisplayForTest: true,
selected: true,
});
@@ -55,7 +57,6 @@ describe('Thumbnail component', () => {
const handleFocusSpy = vi.fn();
render(Thumbnail, {
asset,
overrideDisplayForTest: true,
handleFocus: handleFocusSpy,
});
@@ -70,7 +71,6 @@ describe('Thumbnail component', () => {
const handleFocusSpy = vi.fn();
render(Thumbnail, {
asset,
overrideDisplayForTest: true,
focussed: true,
handleFocus: handleFocusSpy,
});

View File

@@ -4,6 +4,7 @@
import Icon from '$lib/components/elements/icon.svelte';
import { TUNABLES } from '$lib/utils/tunables';
import { mdiEyeOffOutline } from '@mdi/js';
import type { ClassValue } from 'svelte/elements';
import { fade } from 'svelte/transition';
interface Props {
@@ -19,6 +20,7 @@
hidden?: boolean;
border?: boolean;
hiddenIconClass?: string;
class?: ClassValue;
onComplete?: (() => void) | undefined;
}
@@ -36,6 +38,7 @@
border = false,
hiddenIconClass = 'text-white',
onComplete = undefined,
class: imageClass = '',
}: Props = $props();
let {
@@ -88,7 +91,7 @@
src={url}
alt={loaded || errored ? altText : ''}
{title}
class="object-cover {optionalClasses}"
class={['object-cover', optionalClasses, imageClass]}
class:opacity-0={!thumbhash && !loaded}
draggable="false"
/>

View File

@@ -1,12 +1,11 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { ProjectionType } from '$lib/constants';
import { getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { timeToSeconds } from '$lib/utils/date-time';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
import { getAssetPlaybackUrl } from '$lib/utils';
import { getAssetPlaybackUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
import { timeToSeconds } from '$lib/utils/date-time';
import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import {
mdiArchiveArrowDownOutline,
mdiCameraBurst,
@@ -17,13 +16,14 @@
mdiRotate360,
} from '@mdi/js';
import { thumbhash } from '$lib/actions/thumbhash';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
import { TUNABLES } from '$lib/utils/tunables';
import type { ClassValue } from 'svelte/elements';
import { fade } from 'svelte/transition';
import ImageThumbnail from './image-thumbnail.svelte';
import VideoThumbnail from './video-thumbnail.svelte';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
import { TUNABLES } from '$lib/utils/tunables';
import { thumbhash } from '$lib/actions/thumbhash';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
interface Props {
asset: AssetResponseDto;
@@ -35,16 +35,16 @@
focussed?: boolean;
selectionCandidate?: boolean;
disabled?: boolean;
disableLinkMouseOver?: boolean;
readonly?: boolean;
showArchiveIcon?: boolean;
showStackedIcon?: boolean;
disableMouseOver?: boolean;
imageClass?: ClassValue;
dimmed?: boolean;
onClick?: ((asset: AssetResponseDto) => void) | undefined;
onSelect?: ((asset: AssetResponseDto) => void) | undefined;
onMouseEvent?: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined;
handleFocus?: (() => void) | undefined;
class?: string;
}
let {
@@ -57,15 +57,16 @@
focussed = false,
selectionCandidate = false,
disabled = false,
disableLinkMouseOver = false,
readonly = false,
showArchiveIcon = false,
showStackedIcon = true,
disableMouseOver = false,
onClick = undefined,
onSelect = undefined,
onMouseEvent = undefined,
handleFocus = undefined,
class: className = '',
imageClass = '',
dimmed = false,
}: Props = $props();
let {
@@ -145,11 +146,12 @@
<div
data-asset={asset.id}
class={[
'focus-visible:outline-none flex overflow-hidden',
disabled ? 'bg-gray-300' : 'bg-immich-primary/20 dark:bg-immich-dark-primary/20',
]}
style:width="{width}px"
style:height="{height}px"
class="focus-visible:outline-none flex overflow-hidden {disabled
? 'bg-gray-300'
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
>
{#if !loaded && asset.thumbhash}
<canvas
@@ -168,11 +170,9 @@
slow: ??ms
-->
<div
class="group"
style:width="{width}px"
style:height="{height}px"
class:cursor-not-allowed={disabled}
class:cursor-pointer={!disabled}
class={['group absolute top-[0px] bottom-[0px]', { 'curstor-not-allowed': disabled, 'cursor-pointer': !disabled }]}
style:width="inherit"
style:height="inherit"
onmouseenter={onMouseEnter}
onmouseleave={onMouseLeave}
use:longPress={{ onLongPress: () => onSelect?.($state.snapshot(asset)) }}
@@ -184,20 +184,19 @@
onSelect?.(asset);
}
}}
tabindex={0}
onclick={handleClick}
role="link"
bind:this={focussableElement}
onfocus={handleFocus}
data-testid="container-with-tabindex"
tabindex={0}
role="link"
>
{#if !usingMobileDevice && mouseOver && !disableMouseOver}
<!-- Select asset button -->
{#if !usingMobileDevice && mouseOver && !disableLinkMouseOver}
<!-- lazy show the url on mouse over-->
<a
class="absolute z-30 {className} top-[41px]"
class={['absolute z-10 w-full top-0 bottom-0']}
style:cursor="unset"
style:width="{width}px"
style:height="{height}px"
href={currentUrlReplaceAssetId(asset.id)}
onclick={(evt) => evt.preventDefault()}
tabindex={-1}
@@ -205,87 +204,98 @@
>
</a>
{/if}
<div class="absolute z-20 {className}" style:width="{width}px" style:height="{height}px">
<!-- Select asset button -->
{#if !readonly && (mouseOver || selected || selectionCandidate)}
<button
type="button"
onclick={onIconClickedHandler}
class="absolute p-2 focus:outline-none"
class:cursor-not-allowed={disabled}
role="checkbox"
tabindex={-1}
onfocus={handleFocus}
aria-checked={selected}
{disabled}
>
{#if disabled}
<Icon path={mdiCheckCircle} size="24" class="text-zinc-800" />
{:else if selected}
<div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]">
<Icon path={mdiCheckCircle} size="24" class="text-immich-primary" />
</div>
{:else}
<Icon path={mdiCheckCircle} size="24" class="text-white/80 hover:text-white" />
{/if}
</button>
{/if}
</div>
{#if !readonly && (mouseOver || selected || selectionCandidate)}
<button
type="button"
onclick={onIconClickedHandler}
class={['absolute z-20 p-2 focus:outline-none', { 'cursor-not-allowed': disabled }]}
role="checkbox"
tabindex={-1}
onfocus={handleFocus}
aria-checked={selected}
{disabled}
>
{#if disabled}
<Icon path={mdiCheckCircle} size="24" class="text-zinc-800" />
{:else if selected}
<div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]">
<Icon path={mdiCheckCircle} size="24" class="text-immich-primary" />
</div>
{:else}
<Icon path={mdiCheckCircle} size="24" class="text-white/80 hover:text-white" />
{/if}
</button>
{/if}
<div
class="absolute h-full w-full select-none bg-transparent transition-transform"
class:scale-[0.85]={selected}
class:rounded-xl={selected}
class={[
'absolute h-full w-full select-none bg-transparent transition-transform',
{ 'scale-[0.85]': selected },
{ 'rounded-xl': selected },
]}
>
<!-- Gradient overlay on hover -->
{#if !usingMobileDevice}
<!-- icon overlay -->
<div>
<!-- Gradient overlay on hover -->
{#if !usingMobileDevice && !disabled}
<div
class={[
'absolute h-full w-full bg-gradient-to-b from-black/25 via-[transparent_25%] opacity-0 transition-opacity group-hover:opacity-100',
{ 'rounded-xl': selected },
]}
></div>
{/if}
<!-- Dimmed support -->
{#if dimmed && !mouseOver}
<div id="a" class={['absolute h-full w-full z-30 bg-gray-700/40', { 'rounded-xl': selected }]}></div>
{/if}
<!-- Outline on focus -->
<div
class="absolute z-10 h-full w-full bg-gradient-to-b from-black/25 via-[transparent_25%] opacity-0 transition-opacity group-hover:opacity-100"
class:rounded-xl={selected}
class={[
'absolute size-full group-focus-visible:outline outline-4 -outline-offset-4 outline-immich-primary',
{ 'rounded-xl': selected },
]}
></div>
{/if}
<!-- Outline on focus -->
<div
class="absolute size-full group-focus-visible:outline outline-4 -outline-offset-4 outline-immich-primary"
></div>
<!-- Favorite asset star -->
{#if !isSharedLink() && asset.isFavorite}
<div class="absolute bottom-2 left-2 z-10">
<Icon path={mdiHeart} size="24" class="text-white" />
</div>
{/if}
<!-- Favorite asset star -->
{#if !isSharedLink() && asset.isFavorite}
<div class="absolute bottom-2 left-2 z-10">
<Icon path={mdiHeart} size="24" class="text-white" />
</div>
{/if}
{#if !isSharedLink() && showArchiveIcon && asset.isArchived}
<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
</div>
{/if}
{#if !isSharedLink() && showArchiveIcon && asset.isArchived}
<div class={['absolute left-2 z-10', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
</div>
{/if}
{#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
<div class="absolute right-0 top-0 z-20 flex place-items-center gap-1 text-xs font-medium text-white">
<span class="pr-2 pt-2">
<Icon path={mdiRotate360} size="24" />
</span>
</div>
{/if}
<!-- Stacked asset -->
{#if asset.stack && showStackedIcon}
<div
class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == undefined
? 'top-0 right-0'
: 'top-7 right-1'} z-20 flex place-items-center gap-1 text-xs font-medium text-white"
>
<span class="pr-2 pt-2 flex place-items-center gap-1">
<p>{asset.stack.assetCount.toLocaleString($locale)}</p>
<Icon path={mdiCameraBurst} size="24" />
</span>
</div>
{/if}
{#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
<div class="absolute right-0 top-0 z-10 flex place-items-center gap-1 text-xs font-medium text-white">
<span class="pr-2 pt-2">
<Icon path={mdiRotate360} size="24" />
</span>
</div>
{/if}
<!-- Stacked asset -->
{#if asset.stack && showStackedIcon}
<div
class={[
'absolute z-10 flex place-items-center gap-1 text-xs font-medium text-white',
asset.type == AssetTypeEnum.Image && !asset.livePhotoVideoId ? 'top-0 right-0' : 'top-7 right-1',
]}
>
<span class="pr-2 pt-2 flex place-items-center gap-1">
<p>{asset.stack.assetCount.toLocaleString($locale)}</p>
<Icon path={mdiCameraBurst} size="24" />
</span>
</div>
{/if}
</div>
<ImageThumbnail
class={imageClass}
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
altText={$getAltText(asset)}
widthStyle="{width}px"
@@ -293,7 +303,6 @@
curve={selected}
onComplete={() => (loaded = true)}
/>
{#if asset.type === AssetTypeEnum.Video}
<div class="absolute top-0 h-full w-full">
<VideoThumbnail
@@ -304,9 +313,7 @@
playbackOnIconHover={!$playVideoThumbnailOnHover}
/>
</div>
{/if}
{#if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId}
{:else if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId}
<div class="absolute top-0 h-full w-full">
<VideoThumbnail
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}

View File

@@ -7,10 +7,17 @@
* Target for the skip link to move focus to.
*/
target?: string;
/**
* Text for the skip link button.
*/
text?: string;
/**
* Breakpoint at which the skip link is visible. Defaults to always being visible.
*/
breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
}
let { target = 'main', text = $t('skip_to_content') }: Props = $props();
let { target = 'main', text = $t('skip_to_content'), breakpoint }: Props = $props();
let isFocused = $state(false);
@@ -18,6 +25,29 @@
const targetEl = document.querySelector<HTMLElement>(target);
targetEl?.focus();
};
const getBreakpoint = () => {
if (!breakpoint) {
return '';
}
switch (breakpoint) {
case 'sm': {
return 'hidden sm:block';
}
case 'md': {
return 'hidden md:block';
}
case 'lg': {
return 'hidden lg:block';
}
case 'xl': {
return 'hidden xl:block';
}
case '2xl': {
return 'hidden 2xl:block';
}
}
};
</script>
<div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
@@ -25,6 +55,7 @@
size="sm"
rounded="none"
onclick={moveFocus}
class={getBreakpoint()}
onfocus={() => (isFocused = true)}
onblur={() => (isFocused = false)}
>

View File

@@ -22,7 +22,7 @@
<input
{...rest}
{type}
{value}
bind:value
max={max || fallbackMax}
oninput={(e) => (updatedValue = e.currentTarget.value)}
onblur={() => (value = updatedValue)}

View File

@@ -51,7 +51,7 @@
</header>
<main
tabindex="-1"
class="relative grid h-screen grid-cols-[theme(spacing.18)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
class="relative grid h-screen grid-cols-[theme(spacing.0)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
>
{#if sidebar}{@render sidebar()}{:else if admin}
<AdminSideBar />
@@ -62,11 +62,11 @@
<section class="relative">
{#if title || buttons}
<div
class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
class="absolute flex h-16 w-full place-items-center justify-between border-b p-2 dark:border-immich-dark-gray dark:text-immich-dark-fg"
>
<div class="flex gap-2 items-center">
{#if title}
<div class="font-medium" tabindex="-1" id={headerId}>{title}</div>
<div class="font-medium outline-none" tabindex="-1" id={headerId}>{title}</div>
{/if}
{#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
@@ -76,7 +76,7 @@
</div>
{/if}
<div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto" use:useActions={use}>
<div class="{scrollbarClass} absolute {hasTitleClass} w-full overflow-y-auto" use:useActions={use}>
{@render children?.()}
</div>
</section>

View File

@@ -1,6 +1,12 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { AssetBucket, assetSnapshot, assetsSnapshot } from '$lib/stores/assets-store.svelte';
import {
type AssetStore,
type AssetBucket,
assetSnapshot,
assetsSnapshot,
isSelectingAllAssets,
} from '$lib/stores/assets-store.svelte';
import { navigate } from '$lib/utils/navigation';
import { getDateLocaleString } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk';
@@ -22,6 +28,7 @@
withStacked: boolean;
showArchiveIcon: boolean;
bucket: AssetBucket;
assetStore: AssetStore;
assetInteraction: AssetInteraction;
onSelect: ({ title, assets }: { title: string; assets: AssetResponseDto[] }) => void;
@@ -36,6 +43,7 @@
showArchiveIcon,
bucket = $bindable(),
assetInteraction,
assetStore,
onSelect,
onSelectAssets,
onSelectAssetCandidates,
@@ -46,9 +54,9 @@
const transitionDuration = $derived.by(() => (bucket.store.suspendTransitions && !$isUploading ? 0 : 150));
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
const onClick = (assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => {
const onClick = (assetStore: AssetStore, assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => {
if (isSelectionMode || assetInteraction.selectionActive) {
assetSelectHandler(asset, assets, groupTitle);
assetSelectHandler(assetStore, asset, assets, groupTitle);
return;
}
void navigate({ targetRoute: 'current', assetId: asset.id });
@@ -56,7 +64,12 @@
const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => onSelect({ title, assets });
const assetSelectHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => {
const assetSelectHandler = (
assetStore: AssetStore,
asset: AssetResponseDto,
assetsInDateGroup: AssetResponseDto[],
groupTitle: string,
) => {
onSelectAssets(asset);
// Check if all assets are selected in a group to toggle the group selection's icon
@@ -70,6 +83,12 @@
} else {
assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
}
if (assetStore.getAssets().length == assetInteraction.selectedAssets.length) {
isSelectingAllAssets.set(true);
} else {
isSelectingAllAssets.set(false);
}
};
const assetMouseEventHandler = (groupTitle: string, asset: AssetResponseDto | null) => {
@@ -164,8 +183,8 @@
{asset}
{groupIndex}
focussed={assetInteraction.isFocussedAsset(asset.id)}
onClick={(asset) => onClick(dateGroup.getAssets(), dateGroup.groupTitle, asset)}
onSelect={(asset) => assetSelectHandler(asset, dateGroup.getAssets(), dateGroup.groupTitle)}
onClick={(asset) => onClick(assetStore, dateGroup.getAssets(), dateGroup.groupTitle, asset)}
onSelect={(asset) => assetSelectHandler(assetStore, asset, dateGroup.getAssets(), dateGroup.groupTitle)}
onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, assetSnapshot(asset))}
selected={assetInteraction.hasSelectedAsset(asset.id) || dateGroup.bucket.store.albumAssets.has(asset.id)}
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}

View File

@@ -4,7 +4,7 @@
import type { Action } from '$lib/components/asset-viewer/actions/action';
import { AppRoute, AssetAction } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetBucket, assetsSnapshot, AssetStore } from '$lib/stores/assets-store.svelte';
import { AssetBucket, assetsSnapshot, AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
import { isSearchEnabled } from '$lib/stores/search.store';
import { featureFlags } from '$lib/stores/server-config.store';
@@ -456,7 +456,7 @@
lastAssetMouseEvent = asset;
};
const handleGroupSelect = (group: string, assets: AssetResponseDto[]) => {
const handleGroupSelect = (assetStore: AssetStore, group: string, assets: AssetResponseDto[]) => {
if (assetInteraction.selectedGroup.has(group)) {
assetInteraction.removeGroupFromMultiselectGroup(group);
for (const asset of assets) {
@@ -468,6 +468,12 @@
handleSelectAsset(asset);
}
}
if (assetStore.getAssets().length == assetInteraction.selectedAssets.length) {
isSelectingAllAssets.set(true);
} else {
isSelectingAllAssets.set(false);
}
};
const handleSelectAssets = async (asset: AssetResponseDto) => {
@@ -720,7 +726,7 @@
class={[
'scrollbar-hidden h-full overflow-y-auto outline-none',
{ 'm-0': isEmpty },
{ 'ml-4 tall:ml-0': !isEmpty },
{ 'ml-0': !isEmpty },
{ 'mr-[60px]': !isEmpty && !usingMobileDevice },
]}
tabindex="-1"
@@ -774,10 +780,11 @@
{withStacked}
{showArchiveIcon}
{assetInteraction}
{assetStore}
{isSelectionMode}
{singleSelect}
{bucket}
onSelect={({ title, assets }) => handleGroupSelect(title, assets)}
onSelect={({ title, assets }) => handleGroupSelect(assetStore, title, assets)}
onSelectAssetCandidates={handleSelectAssetCandidates}
onSelectAssets={handleSelectAssets}
/>

View File

@@ -38,7 +38,7 @@
<section
id="memory-lane"
bind:this={memoryLaneElement}
class="relative mt-5 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
class="relative mt-5 mx-2 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
style="scrollbar-width:none"
use:resizeObserver={({ width }) => (offsetWidth = width)}
onscroll={onScroll}

View File

@@ -1,3 +1,7 @@
<script lang="ts" module>
export const menuButtonId = 'top-menu-button';
</script>
<script lang="ts">
import { page } from '$app/state';
import { clickOutside } from '$lib/actions/click-outside';
@@ -12,13 +16,14 @@
import { handleLogout } from '$lib/utils/auth';
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
import { Button, IconButton } from '@immich/ui';
import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import ThemeButton from '../theme-button.svelte';
import UserAvatar from '../user-avatar.svelte';
import AccountInfoPanel from './account-info-panel.svelte';
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
interface Props {
@@ -57,11 +62,34 @@
>
<SkipLink text={$t('skip_to_content')} />
<div
class="grid h-full grid-cols-[theme(spacing.18)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
class="grid h-full grid-cols-[theme(spacing.32)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
>
<a data-sveltekit-preload-data="hover" class="ml-4" href={AppRoute.PHOTOS}>
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
</a>
<div class="flex flex-row gap-1 mx-4 items-center">
<div>
<IconButton
id={menuButtonId}
shape="round"
color="secondary"
variant="ghost"
size="medium"
aria-label={$t('main_menu')}
icon={mdiMenu}
onclick={() => {
isSidebarOpen.value = !isSidebarOpen.value;
}}
onmousedown={(event: MouseEvent) => {
if (isSidebarOpen.value) {
// stops event from reaching the default handler when clicking outside of the sidebar
event.stopPropagation();
}
}}
class="md:hidden"
/>
</div>
<a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
</a>
</div>
<div class="flex justify-between gap-4 lg:gap-8 pr-6">
<div class="hidden w-full max-w-5xl flex-1 tall:pl-0 sm:block">
{#if $featureFlags.search}
@@ -80,7 +108,6 @@
href={AppRoute.SEARCH}
id="search-button"
class="sm:hidden"
title={$t('go_to_search')}
aria-label={$t('go_to_search')}
/>
{/if}
@@ -120,7 +147,6 @@
color="secondary"
variant="ghost"
size="medium"
title={$t('support_and_feedback')}
icon={mdiHelpCircleOutline}
onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
aria-label={$t('support_and_feedback')}

View File

@@ -8,7 +8,9 @@
</script>
<!-- Individual Purchase Option -->
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
<div
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
>
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiAccount} size="56" />
<p class="font-semibold text-lg mt-1">{$t('purchase_individual_title')}</p>

View File

@@ -57,7 +57,7 @@
</div>
{/if}
<div class="flex gap-6 mt-4 justify-between">
<div class="flex flex-col sm:flex-row gap-6 mt-4 justify-between">
<ServerPurchaseOptionCard />
<UserPurchaseOptionCard />
</div>

View File

@@ -8,7 +8,9 @@
</script>
<!-- SERVER Purchase Options -->
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
<div
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
>
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiServer} size="56" />
<p class="font-semibold text-lg mt-1">{$t('purchase_server_title')}</p>

View File

@@ -78,7 +78,7 @@
<LicenseModal onClose={() => (isOpen = false)} />
{/if}
<div class="hidden md:block license-status pl-4 text-sm">
<div class="license-status pl-4 text-sm">
{#if $isPurchased && $preferences.purchase.showSupportBadge}
<button
onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
@@ -95,7 +95,7 @@
onmouseleave={() => (hoverButton = false)}
onfocus={onButtonHover}
onblur={() => (hoverButton = false)}
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 w-full"
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 min-w-52 w-full"
>
<div class="flex justify-between w-full place-items-center place-content-center">
<div class="flex place-items-center place-content-center gap-1">
@@ -110,7 +110,7 @@
<div>
<Icon
path={mdiInformationOutline}
class="flex text-immich-primary dark:text-immich-dark-primary font-medium"
class="hidden md:flex text-immich-primary dark:text-immich-dark-primary font-medium"
size="18"
/>
</div>
@@ -123,7 +123,7 @@
{#if showMessage}
<dialog
open
class="w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
class="hidden md:block w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
transition:fade={{ duration: 150 }}
onmouseover={() => (hoverMessage = true)}
onmouseleave={() => (hoverMessage = false)}

View File

@@ -42,7 +42,7 @@
{/if}
<div
class="text-sm hidden group-hover:sm:flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between"
class="text-sm flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between min-w-52 overflow-hidden"
>
{#if $connected}
<div class="flex gap-2 place-items-center place-content-center">

View File

@@ -62,11 +62,9 @@
class="flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
{isSelected
? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
: ''}
pl-5 group-hover:sm:px-5 md:px-5
"
: ''}"
>
<div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
<div class="flex w-full place-items-center gap-4 pl-5 overflow-hidden truncate">
<Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
<span class="text-sm font-medium">{title}</span>
</div>

View File

@@ -1,17 +1,56 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { clickOutside } from '$lib/actions/click-outside';
import { focusTrap } from '$lib/actions/focus-trap';
import { menuButtonId } from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
import { type Snippet } from 'svelte';
interface Props {
children?: Snippet;
}
const mdBreakpoint = 768;
let { children }: Props = $props();
let innerWidth: number = $state(0);
const closeSidebar = (width: number) => {
isSidebarOpen.value = width >= mdBreakpoint;
};
$effect(() => {
closeSidebar(innerWidth);
});
const isHidden = $derived(!isSidebarOpen.value && innerWidth < mdBreakpoint);
const isExpanded = $derived(isSidebarOpen.value && innerWidth < mdBreakpoint);
const handleClickOutside = () => {
if (!isSidebarOpen.value) {
return;
}
closeSidebar(innerWidth);
if (isHidden) {
document.querySelector<HTMLButtonElement>(`#${menuButtonId}`)?.focus();
}
};
</script>
<svelte:window bind:innerWidth />
<section
id="sidebar"
tabindex="-1"
class="immich-scrollbar group relative z-10 flex w-18 flex-col gap-1 overflow-y-auto bg-immich-bg pt-8 max-md:pt-16 transition-all duration-200 dark:bg-immich-dark-bg hover:sm:w-64 hover:sm:border-r hover:sm:pr-6 hover:sm:shadow-2xl hover:sm:dark:border-r-immich-dark-gray md:w-64 md:pr-6 hover:md:border-none hover:md:shadow-none"
class="immich-scrollbar relative z-10 w-0 md:w-[16rem] overflow-y-auto overflow-x-hidden bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg"
class:shadow-2xl={isExpanded}
class:dark:border-r-immich-dark-gray={isExpanded}
class:border-r={isExpanded}
class:w-[min(100vw,16rem)]={isSidebarOpen.value}
inert={isHidden}
use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}
use:focusTrap={{ active: isExpanded }}
>
{@render children?.()}
<div class="pr-6 flex flex-col gap-1 h-max min-h-full">
{@render children?.()}
</div>
</section>

View File

@@ -84,10 +84,7 @@
bind:isSelected={isSharingSelected}
></SideBarLink>
<div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
<p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
<hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
</div>
<p class="text-xs p-6 dark:text-immich-dark-fg">{$t('library').toUpperCase()}</p>
<SideBarLink
title={$t('favorites')}

View File

@@ -46,7 +46,7 @@
</script>
<div
class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
class="storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm min-w-52"
title={$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale, 3),
@@ -54,26 +54,24 @@
},
})}
>
<div class="hidden group-hover:sm:block md:block">
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
{#if userInteraction.serverInfo}
<p class="text-gray-500 dark:text-gray-300">
{$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale),
available: getByteUnitString(availableBytes, $locale),
},
})}
</p>
{#if userInteraction.serverInfo}
<p class="text-gray-500 dark:text-gray-300">
{$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale),
available: getByteUnitString(availableBytes, $locale),
},
})}
</p>
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
</div>
{:else}
<div class="mt-2">
<LoadingSpinner />
</div>
{/if}
</div>
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
</div>
{:else}
<div class="mt-2">
<LoadingSpinner />
</div>
{/if}
</div>

View File

@@ -32,6 +32,7 @@
href={getLink(path)}
title={value}
class={`flex flex-grow place-items-center pl-2 py-1 text-sm rounded-lg hover:bg-slate-200 dark:hover:bg-slate-800 hover:font-semibold ${isTarget ? 'bg-slate-100 dark:bg-slate-700 font-semibold text-immich-primary dark:text-immich-dark-primary' : 'dark:text-gray-200'}`}
data-sveltekit-keepfocus
>
<button type="button" {onclick} class={Object.values(tree).length === 0 ? 'invisible' : ''}>
<Icon path={isOpen ? mdiChevronDown : mdiChevronRight} class="text-gray-400" size={20} />

View File

@@ -38,7 +38,7 @@ export const colorTheme = persisted<ThemeSetting>('color-theme', initialTheme, {
// Locale to use for formatting dates, numbers, etc.
export const locale = persisted<string | undefined>('locale', undefined, {
serializer: {
parse: (text) => text,
parse: (text) => (text == '' ? 'en-US' : text),
stringify: (object) => object ?? '',
},
});

View File

@@ -0,0 +1 @@
export const isSidebarOpen = $state({ value: false });

View File

@@ -486,6 +486,10 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteraction:
break; // Cancelled
}
assetInteraction.selectAssets(assetsSnapshot(bucket.getAssets()));
for (const dateGroup of bucket.dateGroups) {
assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle);
}
}
} catch (error) {
const $t = get(t);

View File

@@ -131,7 +131,7 @@
<UserPageLayout title={data.meta.title}>
{#snippet sidebar()}
<SideBarSection>
<SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} />
<SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} breakpoint="md" />
<section>
<div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
<div class="h-full">

Some files were not shown because too many files have changed in this diff Show More