Compare commits

..

18 Commits

Author SHA1 Message Date
Mees Frensel 328a1e409d update test 2026-06-13 13:18:23 +02:00
Mees Frensel 3fecea0b35 update validation error wording 2026-06-13 13:17:12 +02:00
Mees Frensel 56643a4fee fix: document and validate AV1 parallelism 2026-06-12 14:47:18 +02:00
Timon 714c647937 fix(web): focus on scrollable element on load (#29004)
fix(web): focus on scrollable element on load
2026-06-12 12:59:16 +02:00
Spencer Stingley c56f477a0f fix: Improving scroll behavior on image stacks that overflow the screen (#28885)
Co-authored-by: Spencer Stingley <accounts@blankcanvas.io>
Co-authored-by: Mees Frensel <33722705+meesfrensel@users.noreply.github.com>
2026-06-12 11:50:10 +02:00
shenlong 296cd40da9 refactor: nullable settings key (#28988)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-11 18:05:50 -05:00
Alex a17276fd1e chore: remove incompatibility message warning (#28993)
* chore: remove incompatibility message warning

* lint
2026-06-11 18:05:19 -05:00
Jason Rasmussen c3e23a6b3a fix: schema configuration (#29000) 2026-06-11 18:05:07 -05:00
Jason Rasmussen 13a7b4a276 fix: pump script (#28998) 2026-06-11 18:04:42 -05:00
renovate[bot] 563cff26bf chore(deps): update machine-learning (#28934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-11 14:40:20 -04:00
Daniel Dietzler e81b6778ca fix: workflow page reactivity issues (#28996) 2026-06-11 13:13:25 -05:00
Alex aa6af7ce36 chore: workflow trigger i18n (#28992) 2026-06-11 10:55:18 -05:00
Santo Shakil 59d036a2ed fix(mobile): give android notification channels proper names (#28986) 2026-06-11 15:07:37 +00:00
renovate[bot] 7a5c014558 fix(deps): update typescript-projects (#28627)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-06-11 17:02:54 +02:00
Santo Shakil e2954b6411 fix(mobile): show albums whose assets are all trashed (#28985) 2026-06-11 09:41:02 -05:00
renovate[bot] 0fb18ed241 chore(deps): update dependency commander to v15 (#28936) 2026-06-11 12:18:25 +02:00
renovate[bot] c0b3b08ce6 chore(deps): update exiftool to v35.21.0 (#28933) 2026-06-11 12:16:13 +02:00
Mees Frensel e8a1084e5b fix(web): heatmap layout and date formatting (#28976)
* fix(web): heatmap layout and date formatting

* chore

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2026-06-11 08:36:34 +00:00
86 changed files with 2151 additions and 17128 deletions
+1 -1
View File
@@ -28,4 +28,4 @@ run = "prettier --write ."
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools]
wrangler = "4.91.0"
wrangler = "4.98.0"
+3 -1
View File
@@ -427,7 +427,7 @@
"transcoding_temporal_aq": "Temporal AQ",
"transcoding_temporal_aq_description": "Applies only to NVENC. Temporal Adaptive Quantization increases quality of high-detail, low-motion scenes. May not be compatible with older devices.",
"transcoding_threads": "Threads",
"transcoding_threads_description": "Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0.",
"transcoding_threads_description": "Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0.{av1, select, true { The AV1 encoder uses a number between 16 describing the amount of parallelism, instead of limiting to a specific number of threads.} other {}}",
"transcoding_tone_mapping": "Tone-mapping",
"transcoding_tone_mapping_description": "Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness.",
"transcoding_transcode_policy": "Transcode policy",
@@ -2389,6 +2389,8 @@
"trash_page_title": "Trash ({count})",
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
"trigger": "Trigger",
"trigger_asset_metadata_extraction": "Asset Metadata Extraction",
"trigger_asset_metadata_extraction_description": "Triggered when the EXIF of an asset is extracted",
"trigger_asset_uploaded": "Asset Upload",
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
"trigger_description": "An event that kicks off the workflow",
+4 -4
View File
@@ -1,8 +1,8 @@
ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:121d86b6d08752968a7dddbc708849e5f3a839bbff47f32212b46d2a1d842bab AS builder-cpu
FROM python:3.11-bookworm@sha256:20ec607c68642c64c73269ce245aa0f060913f4129d15d9687850aa158e6269c AS builder-cpu
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS builder-openvino
FROM python:3.13-slim-trixie@sha256:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS builder-openvino
FROM builder-cpu AS builder-cuda
@@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
FROM python:3.11-slim-bookworm@sha256:8dca233de9f3d9bb410665f00a4da6dd06f331083137e0e98ccf227236fcc438 AS prod-cpu
FROM python:3.11-slim-bookworm@sha256:e2d3af735aff6eeee600b1933bedd99da6645fedf572cc12ef4cc1331f2ceebe AS prod-cpu
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
MACHINE_LEARNING_MODEL_ARENA=false
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS prod-openvino
FROM python:3.13-slim-trixie@sha256:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS prod-openvino
RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
+91 -77
View File
@@ -828,34 +828,34 @@ wheels = [
[[package]]
name = "hf-xet"
version = "1.5.0"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
{ url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
{ url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
{ url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
{ url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
{ url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
{ url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
{ url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
{ url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" },
{ url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" },
{ url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" },
{ url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" },
{ url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" },
{ url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" },
{ url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" },
{ url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
{ url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
{ url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
{ url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
{ url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
{ url = "https://files.pythonhosted.org/packages/64/ee/dd9ba7beae1005e54131b7d45263cc74c8a066d47d354e6d58ae9445a388/hf_xet-1.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577", size = 4069485, upload-time = "2026-06-08T23:02:13.193Z" },
{ url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" },
{ url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" },
{ url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" },
{ url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" },
{ url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" },
{ url = "https://files.pythonhosted.org/packages/53/49/2802f8433c9742ce281bddc1e65c02c32268ca3098d66828b05e12e45ee2/hf_xet-1.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff", size = 4017205, upload-time = "2026-06-08T23:02:23.495Z" },
{ url = "https://files.pythonhosted.org/packages/9e/5a/50c71195b9fb883659f596e7252faf4c18c58e753a9013bdbf9bac5d2250/hf_xet-1.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d", size = 3845426, upload-time = "2026-06-08T23:02:25.124Z" },
{ url = "https://files.pythonhosted.org/packages/05/24/5e0c28f80371c17d49fed004597d9d132cb75c1f6f53db2cb95f459d2312/hf_xet-1.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f", size = 4069676, upload-time = "2026-06-08T23:02:26.759Z" },
{ url = "https://files.pythonhosted.org/packages/d2/17/261ba565b6a4d960fb478f61fdf919c0be5824645aaf1c319eca660c1611/hf_xet-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30", size = 3838509, upload-time = "2026-06-08T23:02:28.573Z" },
{ url = "https://files.pythonhosted.org/packages/4e/44/7ffdc2e184b0d41fc0f683ba3936ef669ab63cf242cf36ef50e57d683668/hf_xet-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6", size = 4505881, upload-time = "2026-06-08T23:02:30.257Z" },
{ url = "https://files.pythonhosted.org/packages/63/b6/788060d5aa4d5e671f1a31bf69624c314eb2d8babab3aa562f9e5d53444e/hf_xet-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a", size = 4292995, upload-time = "2026-06-08T23:02:31.993Z" },
{ url = "https://files.pythonhosted.org/packages/22/93/c5540cbd6b55529b7dc42f6734e88cebee21aefbea34128b66229df56c57/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9", size = 4491570, upload-time = "2026-06-08T23:02:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/03/f3/9d8ceab30f44f36c1679b1b8683054c71a0dadc787dbf07421891742d3ca/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59", size = 4711565, upload-time = "2026-06-08T23:02:35.454Z" },
{ url = "https://files.pythonhosted.org/packages/cd/54/27ed9a5e2cc583b4df82f75a03a4df8dbf55f5a9fa1f47f1fadfb20dbeac/hf_xet-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6", size = 4017343, upload-time = "2026-06-08T23:02:37.14Z" },
{ url = "https://files.pythonhosted.org/packages/ae/12/ecb2fc8d45e767580e3a37faa97cb895608b614965567efb4f18cff67e27/hf_xet-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d", size = 3845716, upload-time = "2026-06-08T23:02:39.073Z" },
{ url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" },
{ url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" },
{ url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" },
{ url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" },
{ url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" },
{ url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" },
{ url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" },
]
[[package]]
@@ -873,31 +873,45 @@ wheels = [
[[package]]
name = "httptools"
version = "0.6.4"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" }
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" },
{ url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" },
{ url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" },
{ url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" },
{ url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" },
{ url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" },
{ url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" },
{ url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" },
{ url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" },
{ url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" },
{ url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" },
{ url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" },
{ url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" },
{ url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" },
{ url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" },
{ url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" },
{ url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" },
{ url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" },
{ url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" },
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
{ url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" },
{ url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" },
{ url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" },
{ url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" },
{ url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" },
{ url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" },
{ url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" },
{ url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" },
{ url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" },
{ url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" },
{ url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
{ url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
{ url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
{ url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
{ url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
{ url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
{ url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
{ url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
{ url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
{ url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
{ url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
{ url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
]
[[package]]
@@ -917,7 +931,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
version = "1.17.0"
version = "1.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -931,9 +945,9 @@ dependencies = [
{ name = "typer" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bd/65/9826515abb600b5722bcf53f8b4a2fb58340b1f8bfcaee19f83561c13a44/huggingface_hub-1.17.0.tar.gz", hash = "sha256:fad842b6763ef70ebc3919665b1b9273645203185400a7d6c5eddc2323cc3435", size = 797082, upload-time = "2026-05-28T15:12:13.347Z" }
sdist = { url = "https://files.pythonhosted.org/packages/88/27/629cfe58c582f92ded066c4a07d1a057ff617118ab7973200f770bd853cb/huggingface_hub-1.19.0.tar.gz", hash = "sha256:fd771622182d40977272a923953ee3b1b13538f9f8a7f5d78398f10af0f1c0bd", size = 824721, upload-time = "2026-06-11T12:33:18.665Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/28/d7cef5e477b855c25d415b8f57e5bc7347c7a90cad3acf1725d0c92ca294/huggingface_hub-1.17.0-py3-none-any.whl", hash = "sha256:3b8156d23118e87f6a587648bfbc04f04a12a757ccb4ed298b35c4ae638bf24c", size = 671546, upload-time = "2026-05-28T15:12:11.441Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a5/558da89f66464d8d0229ff497e8b8666977de2d8cf48c28a2862ecf1250f/huggingface_hub-1.19.0-py3-none-any.whl", hash = "sha256:1dc72e1f6b4d6df6b30eb72e57d00514ef453d660f04af2b87f0e67267f31ee0", size = 693398, upload-time = "2026-06-11T12:33:16.695Z" },
]
[[package]]
@@ -2359,11 +2373,11 @@ wheels = [
[[package]]
name = "python-multipart"
version = "0.0.30"
version = "0.0.32"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" },
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
]
[[package]]
@@ -2624,27 +2638,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.15"
version = "0.15.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
{ url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
{ url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
{ url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
{ url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
{ url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
{ url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
{ url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
{ url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
{ url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
{ url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
{ url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
{ url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
{ url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
]
[[package]]
@@ -3100,15 +3114,15 @@ wheels = [
[[package]]
name = "uvicorn"
version = "0.48.0"
version = "0.49.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
{ url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
]
[package.optional-dependencies]
+6 -4
View File
@@ -69,10 +69,12 @@ if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
echo "Pumping Mobile: $CURRENT_MOBILE => $NEXT_MOBILE"
fi
sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
perl -i -p0e "s/(<key>CFBundleShortVersionString<\/key>\s*<string>)$CURRENT_SERVER(<\/string>)/\${1}$NEXT_SERVER\${2}/s" mobile/ios/Runner/Info.plist
sed -i "s/\"android\.injected\.version\.name\" => \".*\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
sed -i "s/\"android\.injected\.version\.code\" => [0-9]\+,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: .*+[0-9]\+$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
# strip prerelease from CFBundleShortVersionString (deploying to testflight _is_ the prerelease)
NEXT_SERVER_SHORT="${NEXT_SERVER%%-*}"
perl -i -p0e "s/(<key>CFBundleShortVersionString<\/key>\s*<string>).*?(<\/string>)/\${1}$NEXT_SERVER_SHORT\${2}/s" mobile/ios/Runner/Info.plist
echo "IMMICH_VERSION=v$NEXT_SERVER" >>"$GITHUB_ENV"
+13 -13
View File
@@ -217,37 +217,37 @@ checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c70773
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
[[tools.pnpm]]
version = "11.4.0"
version = "11.5.2"
backend = "aqua:pnpm/pnpm"
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:cc38ebd5b2610a5744f84576b963c49e6609a8df5aed714ae3de749998d4478c"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-arm64.tar.gz"
checksum = "sha256:7fef0c74081135d777754fccf25272f698e504b26ba0568504846c0cea402f8f"
url = "https://github.com/pnpm/pnpm/releases/download/v11.5.2/pnpm-linux-arm64.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-arm64-musl"]
checksum = "sha256:a1e2ec9123c709fd04b704227cfcf3b50cd2bbbc1bd39d2df414530b5697eb75"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-arm64-musl.tar.gz"
checksum = "sha256:843beed7bca760276d29f8950ca219600995d345dbc93fad8150b3e5f83b74d4"
url = "https://github.com/pnpm/pnpm/releases/download/v11.5.2/pnpm-linux-arm64-musl.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:f3f8d1217eef013bbc71a24d52efb1f1041e4aff55edd80e0b08e25f409305a4"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-x64.tar.gz"
checksum = "sha256:2033a702618c8576dc6bb0f6adb3a67ab506031351ddd59ca50d1bcaf5d13dc5"
url = "https://github.com/pnpm/pnpm/releases/download/v11.5.2/pnpm-linux-x64.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64-musl"]
checksum = "sha256:60010ad00a96b71e20d1618acaca7a71395e710cbd5e88946c030a1d07c56916"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-x64-musl.tar.gz"
checksum = "sha256:0b794b23461c7475f7ffc29c4945692838b51ddadd857a2ace8edf0018798305"
url = "https://github.com/pnpm/pnpm/releases/download/v11.5.2/pnpm-linux-x64-musl.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:ba59014c2c1ce8b76af9f559385206a2623de4ff2b694b5c91598a8f44abb4e2"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-darwin-arm64.tar.gz"
checksum = "sha256:54993dae26bea0f3c1b0e15f9427f6f6a86827d56f32d1d1554d8cda59a62399"
url = "https://github.com/pnpm/pnpm/releases/download/v11.5.2/pnpm-darwin-arm64.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:84ce90e38bc0b1164173eb853a0fbffc7edcb050cb0d5c8ce4ca609f5c808e0a"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-win32-x64.zip"
checksum = "sha256:b3ddff2c2bf87d3996fadf074bac58cd2259f718a17912a04ae930e3775b30e9"
url = "https://github.com/pnpm/pnpm/releases/download/v11.5.2/pnpm-win32-x64.zip"
provenance = "github-attestations"
[[tools.terragrunt]]
+1 -1
View File
@@ -16,7 +16,7 @@ config_roots = [
[tools]
node = "24.15.0"
pnpm = "11.4.0"
pnpm = "11.5.2"
terragrunt = "1.0.3"
opentofu = "1.11.6"
"npm:oazapfts" = "7.5.0"
@@ -69,7 +69,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
val notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_ID,
ctx.getString(R.string.background_worker_notification_channel_name),
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(notificationChannel)
@@ -5,4 +5,9 @@
<string name="memory_widget_description">See memories from Immich.</string>
<string name="random_widget_description">View a random image from your library or a specific album.</string>
<string name="bg_downloader_notification_channel_name">Uploads and downloads</string>
<string name="bg_downloader_notification_channel_description">Progress updates for uploads and downloads</string>
<string name="background_worker_notification_channel_name">Background backup</string>
</resources>
File diff suppressed because it is too large Load Diff
@@ -40,7 +40,7 @@ void main() {
tearDown(() async {
await workerManagerPatch.dispose();
await server.close();
await Store.delete(StoreKey.legacyServerEndpoint);
await Store.delete(StoreKey.serverEndpoint);
await Store.delete(StoreKey.syncMigrationStatus);
});
@@ -119,9 +119,7 @@ void main() {
final releaseTxn = Completer<void>();
final txnHeld = Completer<void>();
final txn = drift.transaction(() async {
await drift
.into(drift.userEntity)
.insert(
await drift.into(drift.userEntity).insert(
UserEntityCompanion.insert(
id: 'holder',
name: 'holder',
@@ -1,63 +0,0 @@
import 'package:immich_mobile/domain/models/value_codec.dart';
import 'package:immich_mobile/utils/option.dart';
enum SessionKey<T> {
serverUrl<String?>(),
accessToken<String?>(),
serverEndpoint<String?>();
ValueCodec<T> get _codec => ValueCodec.forType(T);
String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw);
}
const defaultSession = Session();
class Session {
final String? serverUrl;
final String? accessToken;
final String? serverEndpoint;
const Session({this.serverUrl, this.accessToken, this.serverEndpoint});
Session copyWith({Option<String>? serverUrl, Option<String>? accessToken, Option<String>? serverEndpoint}) => .new(
serverUrl: serverUrl.patch(this.serverUrl),
accessToken: accessToken.patch(this.accessToken),
serverEndpoint: serverEndpoint.patch(this.serverEndpoint),
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Session &&
other.serverUrl == serverUrl &&
other.accessToken == accessToken &&
other.serverEndpoint == serverEndpoint);
@override
int get hashCode => Object.hash(serverUrl, accessToken, serverEndpoint);
@override
String toString() => 'Session(serverUrl: $serverUrl, accessToken: $accessToken, serverEndpoint: $serverEndpoint)';
T read<T>(SessionKey<T> key) =>
(switch (key) {
.serverUrl => serverUrl,
.accessToken => accessToken,
.serverEndpoint => serverEndpoint,
})
as T;
factory Session.fromEntries(Map<SessionKey, Object?> overrides) =>
overrides.entries.fold(const Session(), (session, entry) => session.write(entry.key, entry.value));
Session write<T, U extends T>(SessionKey<T> key, U value) {
return switch (key) {
.serverUrl => copyWith(serverUrl: .fromNullable(value as String?)),
.accessToken => copyWith(accessToken: .fromNullable(value as String?)),
.serverEndpoint => copyWith(serverEndpoint: .fromNullable(value as String?)),
};
}
}
+158 -17
View File
@@ -1,15 +1,16 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/models/value_codec.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
enum SettingsKey<T> {
// Theme
themePrimaryColor<ImmichColorPreset>(codec: EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
themeDynamic<bool>(),
themeColorfulInterface<bool>(),
@@ -25,13 +26,13 @@ enum SettingsKey<T> {
// Network
networkAutoEndpointSwitching<bool>(),
networkExternalEndpointList<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
networkCustomHeaders<Map<String, String>>(codec: MapCodec(PrimitiveCodec.string, PrimitiveCodec.string)),
networkPreferredWifiName<String?>(),
networkLocalEndpoint<String?>(),
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
// Album
albumSortMode<AlbumSortMode>(codec: EnumCodec(AlbumSortMode.values)),
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
albumIsReverse<bool>(),
albumIsGrid<bool>(),
@@ -45,43 +46,183 @@ enum SettingsKey<T> {
// Timeline
timelineTilesPerRow<int>(),
timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)),
timelineStorageIndicator<bool>(),
// Log
logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
// Map
mapShowFavoriteOnly<bool>(),
mapRelativeDate<int>(),
mapIncludeArchived<bool>(),
mapThemeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
mapWithPartners<bool>(),
// Cleanup
cleanupKeepFavorites<bool>(),
cleanupKeepMediaType<AssetKeepType>(codec: EnumCodec(AssetKeepType.values)),
cleanupKeepAlbumIds<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)),
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
cleanupCutoffDaysAgo<int>(),
cleanupDefaultsInitialized<bool>(),
// Share
shareFileType<ShareAssetType>(codec: EnumCodec(ShareAssetType.values)),
shareFileType<ShareAssetType>(codec: _EnumCodec(ShareAssetType.values)),
// Slideshow
slideshowTransition<bool>(),
slideshowRepeat<bool>(),
slideshowDuration<int>(),
slideshowLook<SlideshowLook>(codec: EnumCodec(SlideshowLook.values)),
slideshowDirection<SlideshowDirection>(codec: EnumCodec(SlideshowDirection.values));
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
final ValueCodec<T>? _codecOverride;
final _SettingsCodec<T>? _codecOverride;
const SettingsKey({ValueCodec<T>? codec}) : _codecOverride = codec;
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
ValueCodec<T> get _codec => _codecOverride ?? ValueCodec.forType(T);
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw);
}
sealed class _SettingsCodec<T> {
const _SettingsCodec();
String encode(T value);
T decode(String raw);
static final Map<Type, _SettingsCodec<Object>> _primitives = {
..._register<int>(_PrimitiveCodec.integer),
..._register<double>(_PrimitiveCodec.real),
..._register<bool>(_PrimitiveCodec.boolean),
..._register<String>(_PrimitiveCodec.string),
..._register<DateTime>(const _DateTimeCodec()),
};
static Map<Type, _SettingsCodec<Object>> _register<T>(_SettingsCodec<Object> codec) => {
T: codec,
// Reifies the nullable type T so it can be used as a key in the _primitives map
_typeOf<T?>(): codec,
};
static Type _typeOf<T>() => T;
static _SettingsCodec<T> forType<T>(Type runtimeType) {
final codec = _primitives[runtimeType];
if (codec == null) {
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
}
return codec as _SettingsCodec<T>;
}
}
final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
final List<T> values;
const _EnumCodec(this.values);
@override
String encode(T value) => value.name;
@override
T decode(String raw) => values.firstWhere((v) => v.name == raw);
}
final class _DateTimeCodec extends _SettingsCodec<DateTime> {
const _DateTimeCodec();
@override
String encode(DateTime value) => value.toIso8601String();
@override
DateTime decode(String raw) => DateTime.parse(raw);
}
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
final _SettingsCodec<K> _keyCodec;
final _SettingsCodec<V> _valueCodec;
const _MapCodec(this._keyCodec, this._valueCodec);
@override
String encode(Map<K, V> value) {
final entries = <String, String>{};
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
return jsonEncode(entries);
}
@override
Map<K, V> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) {
return {};
}
final result = <K, V>{};
for (final entry in decoded.entries) {
final rawKey = entry.key;
final rawValue = entry.value;
if (rawKey is! String || rawValue is! String) {
return {};
}
final k = _keyCodec.decode(rawKey);
final v = _valueCodec.decode(rawValue);
result[k] = v;
}
return result;
} on FormatException {
return {};
}
}
}
final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
final _SettingsCodec<T> _elementCodec;
const _ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) {
return [];
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) {
return [];
}
final element = _elementCodec.decode(item);
result.add(element);
}
return result;
} on FormatException {
return [];
}
}
}
final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
final T Function(String) _parse;
const _PrimitiveCodec._(this._parse);
@override
String encode(T value) => value.toString();
@override
T decode(String raw) => _parse(raw);
static const integer = _PrimitiveCodec<int>._(int.parse);
static const real = _PrimitiveCodec<double>._(double.parse);
static const boolean = _PrimitiveCodec<bool>._(bool.parse);
static const string = _PrimitiveCodec<String>._(_identity);
static String _identity(String s) => s;
}
+6 -3
View File
@@ -1,8 +1,14 @@
import 'package:immich_mobile/domain/models/user.model.dart';
/// Key for each possible value in the `Store`.
/// Defines the data type for each value
enum StoreKey<T> {
version<int>._(0),
currentUser<UserDto>._(2),
deviceId<String>._(4),
serverUrl<String>._(10),
accessToken<String>._(11),
serverEndpoint<String>._(12),
advancedTroubleshooting<bool>._(114),
enableHapticFeedback<bool>._(126),
@@ -13,9 +19,6 @@ enum StoreKey<T> {
syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store
legacyServerUrl<String>._(10),
legacyAccessToken<String>._(11),
legacyServerEndpoint<String>._(12),
legacyBackupRequireCharging<bool>._(7),
legacyBackupTriggerDelay<int>._(8),
legacySyncAlbums<bool>._(131),
-141
View File
@@ -1,141 +0,0 @@
import 'dart:convert';
sealed class ValueCodec<T> {
const ValueCodec();
String encode(T value);
T decode(String raw);
static final Map<Type, ValueCodec<Object>> _primitives = {
..._register<int>(PrimitiveCodec.integer),
..._register<double>(PrimitiveCodec.real),
..._register<bool>(PrimitiveCodec.boolean),
..._register<String>(PrimitiveCodec.string),
..._register<DateTime>(const DateTimeCodec()),
};
static Map<Type, ValueCodec<Object>> _register<T>(ValueCodec<Object> codec) => {
T: codec,
// Reifies the nullable type T so it can be used as a key in the _primitives map
_typeOf<T?>(): codec,
};
static Type _typeOf<T>() => T;
static ValueCodec<T> forType<T>(Type runtimeType) {
final codec = _primitives[runtimeType];
if (codec == null) {
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the key.');
}
return codec as ValueCodec<T>;
}
}
final class EnumCodec<T extends Enum> extends ValueCodec<T> {
final List<T> values;
const EnumCodec(this.values);
@override
String encode(T value) => value.name;
@override
T decode(String raw) => values.firstWhere((v) => v.name == raw);
}
final class DateTimeCodec extends ValueCodec<DateTime> {
const DateTimeCodec();
@override
String encode(DateTime value) => value.toIso8601String();
@override
DateTime decode(String raw) => DateTime.parse(raw);
}
final class MapCodec<K extends Object, V extends Object> extends ValueCodec<Map<K, V>> {
final ValueCodec<K> _keyCodec;
final ValueCodec<V> _valueCodec;
const MapCodec(this._keyCodec, this._valueCodec);
@override
String encode(Map<K, V> value) {
final entries = <String, String>{};
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
return jsonEncode(entries);
}
@override
Map<K, V> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) {
return {};
}
final result = <K, V>{};
for (final entry in decoded.entries) {
final rawKey = entry.key;
final rawValue = entry.value;
if (rawKey is! String || rawValue is! String) {
continue;
}
final k = _keyCodec.decode(rawKey);
final v = _valueCodec.decode(rawValue);
result[k] = v;
}
return result;
} on FormatException {
return {};
}
}
}
final class ListCodec<T extends Object> extends ValueCodec<List<T>> {
final ValueCodec<T> _elementCodec;
const ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) {
return [];
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) {
return [];
}
final element = _elementCodec.decode(item);
result.add(element);
}
return result;
} on FormatException {
return [];
}
}
}
final class PrimitiveCodec<T extends Object> extends ValueCodec<T> {
final T Function(String) _parse;
const PrimitiveCodec._(this._parse);
@override
String encode(T value) => value.toString();
@override
T decode(String raw) => _parse(raw);
static const integer = PrimitiveCodec<int>._(int.parse);
static const real = PrimitiveCodec<double>._(double.parse);
static const boolean = PrimitiveCodec<bool>._(bool.parse);
static const string = PrimitiveCodec<String>._(_identity);
static String _identity(String s) => s;
}
@@ -2,12 +2,13 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
@@ -17,7 +18,7 @@ final syncLinkedAlbumServiceProvider = Provider(
ref.watch(localAlbumRepository),
ref.watch(remoteAlbumRepository),
ref.watch(driftAlbumApiRepositoryProvider),
ref.watch(authUserRepositoryProvider),
ref.watch(storeServiceProvider),
cancellation: ref.watch(cancellationProvider),
),
);
@@ -26,14 +27,14 @@ class SyncLinkedAlbumService {
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftAlbumApiRepository _albumApiRepository;
final DriftAuthUserRepository _authUserRepository;
final StoreService _storeService;
final Completer<void>? _cancellation;
SyncLinkedAlbumService(
this._localAlbumRepository,
this._remoteAlbumRepository,
this._albumApiRepository,
this._authUserRepository, {
this._storeService, {
this._cancellation,
});
@@ -122,12 +123,11 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
final currentUser = await _authUserRepository.get();
if (currentUser == null) {
_log.warning("No user logged in, skipping remote album creation for local album: ${localAlbum.name}");
return;
}
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, currentUser, assetIds: []);
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(
localAlbum.name,
_storeService.get(StoreKey.currentUser),
assetIds: [],
);
await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
}
+14 -11
View File
@@ -1,24 +1,29 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:logging/logging.dart';
class UserService {
final Logger _log = Logger("UserService");
final UserApiRepository _userApiRepository;
final DriftAuthUserRepository _authUserRepository;
final StoreService _storeService;
UserService({required this._userApiRepository, required this._authUserRepository});
UserService({required this._userApiRepository, required this._storeService});
Future<UserDto?> tryGetMyUser() {
return _authUserRepository.get();
UserDto getMyUser() {
return _storeService.get(StoreKey.currentUser);
}
UserDto? tryGetMyUser() {
return _storeService.tryGet(StoreKey.currentUser);
}
Stream<UserDto?> watchMyUser() {
return _authUserRepository.watch();
return _storeService.watch(StoreKey.currentUser);
}
Future<UserDto?> refreshMyUser() async {
@@ -26,17 +31,15 @@ class UserService {
if (user == null) {
return null;
}
await _authUserRepository.upsert(user);
await _storeService.put(StoreKey.currentUser, user);
return user;
}
Future<String?> createProfileImage(String name, Uint8List image) async {
try {
final path = await _userApiRepository.createProfileImage(name: name, data: image);
final updatedUser = await tryGetMyUser();
if (updatedUser != null) {
await _authUserRepository.upsert(updatedUser);
}
final updatedUser = getMyUser();
await _storeService.put(StoreKey.currentUser, updatedUser);
return path;
} catch (e) {
_log.warning("Failed to upload profile image", e);
@@ -1,13 +1,14 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:logging/logging.dart';
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) async {
final user = await ref.read(authUserRepositoryProvider).get();
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
final user = Store.tryGet(StoreKey.currentUser);
if (user == null) {
Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync");
return;
return Future.value();
}
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
}
@@ -1,18 +0,0 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class SessionEntity extends Table with DriftDefaultsMixin {
const SessionEntity();
TextColumn get key => text()();
TextColumn get value => text().nullable()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {key};
@override
String get tableName => "session";
}
@@ -1,427 +0,0 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/session.entity.dart'
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$SessionEntityTableCreateCompanionBuilder =
i1.SessionEntityCompanion Function({
required String key,
i0.Value<String?> value,
i0.Value<DateTime> updatedAt,
});
typedef $$SessionEntityTableUpdateCompanionBuilder =
i1.SessionEntityCompanion Function({
i0.Value<String> key,
i0.Value<String?> value,
i0.Value<DateTime> updatedAt,
});
class $$SessionEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
$$SessionEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$SessionEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
$$SessionEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$SessionEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
$$SessionEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get key =>
$composableBuilder(column: $table.key, builder: (column) => column);
i0.GeneratedColumn<String> get value =>
$composableBuilder(column: $table.value, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
class $$SessionEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData,
i1.$$SessionEntityTableFilterComposer,
i1.$$SessionEntityTableOrderingComposer,
i1.$$SessionEntityTableAnnotationComposer,
$$SessionEntityTableCreateCompanionBuilder,
$$SessionEntityTableUpdateCompanionBuilder,
(
i1.SessionEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData
>,
),
i1.SessionEntityData,
i0.PrefetchHooks Function()
> {
$$SessionEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$SessionEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$SessionEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$SessionEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$SessionEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> key = const i0.Value.absent(),
i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.SessionEntityCompanion(
key: key,
value: value,
updatedAt: updatedAt,
),
createCompanionCallback:
({
required String key,
i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.SessionEntityCompanion.insert(
key: key,
value: value,
updatedAt: updatedAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$SessionEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData,
i1.$$SessionEntityTableFilterComposer,
i1.$$SessionEntityTableOrderingComposer,
i1.$$SessionEntityTableAnnotationComposer,
$$SessionEntityTableCreateCompanionBuilder,
$$SessionEntityTableUpdateCompanionBuilder,
(
i1.SessionEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData
>,
),
i1.SessionEntityData,
i0.PrefetchHooks Function()
>;
class $SessionEntityTable extends i2.SessionEntity
with i0.TableInfo<$SessionEntityTable, i1.SessionEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$SessionEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
@override
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
'key',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _valueMeta = const i0.VerificationMeta(
'value',
);
@override
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
'value',
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: i3.currentDateAndTime,
);
@override
List<i0.GeneratedColumn> get $columns => [key, value, updatedAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'session';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.SessionEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('key')) {
context.handle(
_keyMeta,
key.isAcceptableOrUnknown(data['key']!, _keyMeta),
);
} else if (isInserting) {
context.missing(_keyMeta);
}
if (data.containsKey('value')) {
context.handle(
_valueMeta,
value.isAcceptableOrUnknown(data['value']!, _valueMeta),
);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {key};
@override
i1.SessionEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.SessionEntityData(
key: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}key'],
)!,
value: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}value'],
),
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
);
}
@override
$SessionEntityTable createAlias(String alias) {
return $SessionEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class SessionEntityData extends i0.DataClass
implements i0.Insertable<i1.SessionEntityData> {
final String key;
final String? value;
final DateTime updatedAt;
const SessionEntityData({
required this.key,
this.value,
required this.updatedAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['key'] = i0.Variable<String>(key);
if (!nullToAbsent || value != null) {
map['value'] = i0.Variable<String>(value);
}
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
return map;
}
factory SessionEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return SessionEntityData(
key: serializer.fromJson<String>(json['key']),
value: serializer.fromJson<String?>(json['value']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'key': serializer.toJson<String>(key),
'value': serializer.toJson<String?>(value),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
};
}
i1.SessionEntityData copyWith({
String? key,
i0.Value<String?> value = const i0.Value.absent(),
DateTime? updatedAt,
}) => i1.SessionEntityData(
key: key ?? this.key,
value: value.present ? value.value : this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
SessionEntityData copyWithCompanion(i1.SessionEntityCompanion data) {
return SessionEntityData(
key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
);
}
@override
String toString() {
return (StringBuffer('SessionEntityData(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(key, value, updatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.SessionEntityData &&
other.key == this.key &&
other.value == this.value &&
other.updatedAt == this.updatedAt);
}
class SessionEntityCompanion extends i0.UpdateCompanion<i1.SessionEntityData> {
final i0.Value<String> key;
final i0.Value<String?> value;
final i0.Value<DateTime> updatedAt;
const SessionEntityCompanion({
this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
});
SessionEntityCompanion.insert({
required String key,
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
}) : key = i0.Value(key);
static i0.Insertable<i1.SessionEntityData> custom({
i0.Expression<String>? key,
i0.Expression<String>? value,
i0.Expression<DateTime>? updatedAt,
}) {
return i0.RawValuesInsertable({
if (key != null) 'key': key,
if (value != null) 'value': value,
if (updatedAt != null) 'updated_at': updatedAt,
});
}
i1.SessionEntityCompanion copyWith({
i0.Value<String>? key,
i0.Value<String?>? value,
i0.Value<DateTime>? updatedAt,
}) {
return i1.SessionEntityCompanion(
key: key ?? this.key,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (key.present) {
map['key'] = i0.Variable<String>(key.value);
}
if (value.present) {
map['value'] = i0.Variable<String>(value.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SessionEntityCompanion(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
}
@@ -1,42 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
// ignore: depend_on_referenced_packages
import 'package:meta/meta.dart';
abstract class CachedKeyValueRepository<K extends Enum, S> {
CachedKeyValueRepository(this._snapshot);
S _snapshot;
S get snapshot => _snapshot;
@protected
set snapshot(S value) => _snapshot = value;
List<K> get keys;
Object decodeValue(K key, String raw);
S buildSnapshot(Map<K, Object?> overrides);
Selectable<({String key, String? value})> selectable();
Future<void> refresh() async => _snapshot = _build(await selectable().get());
@protected
Stream<S> watchSnapshot() => selectable().watch().map((rows) => _snapshot = _build(rows));
S _build(List<({String key, String? value})> rows) => buildSnapshot(
rows.fold({}, (overrides, row) {
final key = keys.firstWhereOrNull((key) => key.name == row.key);
if (key == null) {
return overrides;
}
Object? decodedValue;
if (row.value != null) {
decodedValue = decodeValue(key, row.value!);
}
return {...overrides, key: decodedValue};
}),
);
}
@@ -25,7 +25,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
@@ -68,7 +67,6 @@ import 'package:sqlite_async/sqlite_async.dart';
AssetEditEntity,
SettingsEntity,
AssetOcrEntity,
SessionEntity,
],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
)
@@ -122,7 +120,7 @@ class Drift extends $Drift {
}
@override
int get schemaVersion => 31;
int get schemaVersion => 30;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -313,9 +311,6 @@ class Drift extends $Drift {
from29To30: (m, v30) async {
await m.alterTable(TableMigration(v30.settings));
},
from30To31: (m, v31) async {
await m.createTable(v31.session);
},
),
);
@@ -47,11 +47,9 @@ import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart
as i22;
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'
as i23;
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'
as i24;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i25;
import 'package:drift/internal/modular.dart' as i26;
as i24;
import 'package:drift/internal/modular.dart' as i25;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -101,12 +99,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i23.$AssetOcrEntityTable assetOcrEntity = i23.$AssetOcrEntityTable(
this,
);
late final i24.$SessionEntityTable sessionEntity = i24.$SessionEntityTable(
i24.MergedAssetDrift get mergedAssetDrift => i25.ReadDatabaseContainer(
this,
);
i25.MergedAssetDrift get mergedAssetDrift => i26.ReadDatabaseContainer(
this,
).accessor<i25.MergedAssetDrift>(i25.MergedAssetDrift.new);
).accessor<i24.MergedAssetDrift>(i24.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -145,7 +140,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
assetEditEntity,
settingsEntity,
assetOcrEntity,
sessionEntity,
i10.idxPartnerSharedWithId,
i11.idxLatLng,
i11.idxRemoteExifCity,
@@ -420,6 +414,4 @@ class $DriftManager {
i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity);
i23.$$AssetOcrEntityTableTableManager get assetOcrEntity =>
i23.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity);
i24.$$SessionEntityTableTableManager get sessionEntity =>
i24.$$SessionEntityTableTableManager(_db, _db.sessionEntity);
}
@@ -15920,599 +15920,6 @@ i1.GeneratedColumn<String> _column_224(String aliasedName) =>
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
final class Schema31 extends i0.VersionedSchema {
Schema31({required super.database}) : super(version: 31);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxLocalAssetCreatedAt,
idxStackPrimaryAssetId,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
settings,
assetOcrEntity,
session,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
idxAssetOcrAssetId,
];
late final Shape33 userEntity = Shape33(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape50 remoteAssetEntity = Shape50(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_212,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 stackEntity = Shape35(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_130,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 localAssetEntity = Shape36(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_131,
_column_120,
_column_132,
_column_133,
_column_134,
_column_135,
_column_136,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 remoteAlbumEntity = Shape48(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_138,
_column_114,
_column_115,
_column_139,
_column_140,
_column_141,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 localAlbumEntity = Shape38(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_115,
_column_142,
_column_143,
_column_144,
_column_145,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape39 localAlbumAssetEntity = Shape39(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_146, _column_147, _column_145],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxLocalAssetCreatedAt = i1.Index(
'idx_local_asset_created_at',
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_148,
_column_110,
_column_111,
_column_149,
_column_150,
_column_151,
_column_152,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_153, _column_154, _column_155],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 partnerEntity = Shape41(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_156, _column_157, _column_158],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 remoteExifEntity = Shape42(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_160,
_column_161,
_column_162,
_column_163,
_column_164,
_column_117,
_column_116,
_column_165,
_column_166,
_column_167,
_column_168,
_column_135,
_column_136,
_column_169,
_column_170,
_column_171,
_column_172,
_column_173,
_column_174,
_column_175,
_column_176,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_159, _column_177],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_177, _column_153, _column_178],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 remoteAssetCloudIdEntity = Shape43(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_179,
_column_180,
_column_134,
_column_135,
_column_136,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 memoryEntity = Shape44(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_124,
_column_121,
_column_113,
_column_181,
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_159, _column_187],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 personEntity = Shape45(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_108,
_column_188,
_column_189,
_column_190,
_column_191,
_column_192,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 assetFaceEntity = Shape46(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_193,
_column_194,
_column_195,
_column_196,
_column_197,
_column_198,
_column_199,
_column_200,
_column_201,
_column_124,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_202, _column_203, _column_204],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 trashedLocalAssetEntity = Shape47(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_205,
_column_131,
_column_120,
_column_132,
_column_206,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 assetEditEntity = Shape32(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_207,
_column_208,
_column_209,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 settings = Shape49(
source: i0.VersionedTable(
entityName: 'settings',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_224, _column_115],
attachedDatabase: database,
),
alias: null,
);
late final Shape51 assetOcrEntity = Shape51(
source: i0.VersionedTable(
entityName: 'asset_ocr_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_213,
_column_214,
_column_215,
_column_216,
_column_217,
_column_218,
_column_219,
_column_220,
_column_221,
_column_222,
_column_223,
_column_201,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 session = Shape49(
source: i0.VersionedTable(
entityName: 'session',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_224, _column_115],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
final i1.Index idxAssetEditAssetId = i1.Index(
'idx_asset_edit_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
);
final i1.Index idxAssetOcrAssetId = i1.Index(
'idx_asset_ocr_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
);
}
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -16543,7 +15950,6 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -16692,11 +16098,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from29To30(migrator, schema);
return 30;
case 30:
final schema = Schema31(database: database);
final migrator = i1.Migrator(database, schema);
await from30To31(migrator, schema);
return 31;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -16733,7 +16134,6 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -16765,6 +16165,5 @@ i1.OnUpgrade stepByStep({
from27To28: from27To28,
from28To29: from28To29,
from29To30: from29To30,
from30To31: from30To31,
),
);
@@ -20,7 +20,10 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
const DriftRemoteAlbumRepository(this._db) : super(_db);
Future<List<RemoteAlbum>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {SortRemoteAlbumsBy.updatedAt}}) {
final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true);
// Count non-trashed assets via the joined asset table. Filtering trashed assets in the
// join condition (instead of the where clause) keeps albums whose assets are all trashed
// in the result, the same way truly empty albums are kept
final assetCount = _db.remoteAssetEntity.id.count(distinct: true);
final query = _db.remoteAlbumEntity.select().join([
leftOuterJoin(
@@ -30,7 +33,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull(),
useColumns: false,
),
leftOuterJoin(
@@ -47,7 +51,6 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
),
]);
query
..where(_db.remoteAssetEntity.deletedAt.isNull())
..addColumns([assetCount])
..addColumns([_db.userEntity.name, _db.userEntity.id])
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
@@ -79,7 +82,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
}
Future<RemoteAlbum?> get(String albumId) {
final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true);
final assetCount = _db.remoteAssetEntity.id.count(distinct: true);
final query =
_db.remoteAlbumEntity.select().join([
@@ -90,7 +93,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull(),
useColumns: false,
),
leftOuterJoin(
@@ -106,7 +110,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
useColumns: false,
),
])
..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull())
..where(_db.remoteAlbumEntity.id.equals(albumId))
..addColumns([assetCount])
..addColumns([_db.userEntity.name, _db.userEntity.id])
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
@@ -515,7 +519,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
return [];
}
final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true);
final assetCount = _db.remoteAssetEntity.id.count(distinct: true);
final query =
_db.remoteAlbumEntity.select().join([
leftOuterJoin(
@@ -525,7 +529,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull(),
useColumns: false,
),
leftOuterJoin(
@@ -541,7 +546,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
useColumns: false,
),
])
..where(_db.remoteAlbumEntity.id.isIn(albumIds) & _db.remoteAssetEntity.deletedAt.isNull())
..where(_db.remoteAlbumEntity.id.isIn(albumIds))
..addColumns([assetCount])
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
..addColumns([_db.userEntity.name, _db.userEntity.id])
@@ -1,82 +0,0 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class SessionRepository extends CachedKeyValueRepository<SessionKey, Session> {
final Drift _db;
SessionRepository._(this._db) : super(const .new());
static SessionRepository? _instance;
static SessionRepository get instance {
final instance = _instance;
if (instance == null) {
throw StateError('SessionRepository not initialized. Call ensureInitialized() first');
}
return instance;
}
static Future<SessionRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = SessionRepository._(db);
await instance.refresh();
_instance = instance;
}
return _instance!;
}
@override
List<SessionKey> get keys => SessionKey.values;
@override
Object decodeValue(SessionKey key, String raw) => key.decode(raw);
@override
Session buildSnapshot(Map<SessionKey, Object?> overrides) => Session.fromEntries(overrides);
@override
@protected
Selectable<({String key, String? value})> selectable() =>
_db.select(_db.sessionEntity).map((row) => (key: row.key, value: row.value));
Session get session => snapshot;
Future<void> clear(Iterable<SessionKey> keys) async {
if (keys.isEmpty) {
return;
}
final names = keys.map((key) => key.name).toList();
await (_db.delete(_db.sessionEntity)..where((row) => row.key.isIn(names))).go();
var session = snapshot;
for (final key in keys) {
session = session.write(key, defaultSession.read(key));
}
snapshot = session;
}
Future<void> write<T, U extends T>(SessionKey<T> key, U value) async {
if (value == snapshot.read(key)) {
return;
}
String? resolvedValue;
if (value != null) {
resolvedValue = key.encode(value);
}
await _db
.into(_db.sessionEntity)
.insertOnConflictUpdate(
SessionEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
);
snapshot = snapshot.write(key, value);
}
Stream<Session> watch() => watchSnapshot();
}
@@ -1,15 +1,13 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig> {
class SettingsRepository extends DriftDatabaseRepository {
final Drift _db;
SettingsRepository._(this._db) : super(const .new());
SettingsRepository._(this._db) : super(_db);
static SettingsRepository? _instance;
@@ -21,6 +19,9 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
return instance;
}
AppConfig _appConfig = const .new();
AppConfig get appConfig => _appConfig;
static Future<SettingsRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = SettingsRepository._(db);
@@ -30,21 +31,7 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
return _instance!;
}
@override
List<SettingsKey> get keys => SettingsKey.values;
@override
Object decodeValue(SettingsKey key, String raw) => key.decode(raw);
@override
AppConfig buildSnapshot(Map<SettingsKey, Object?> overrides) => AppConfig.fromEntries(overrides);
@override
@protected
Selectable<({String key, String? value})> selectable() =>
_db.select(_db.settingsEntity).map((row) => (key: row.key, value: row.value));
AppConfig get appConfig => snapshot;
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get());
Future<void> clear(Iterable<SettingsKey> keys) async {
if (keys.isEmpty) {
@@ -54,15 +41,13 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
final names = keys.map((key) => key.name).toList();
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
var config = snapshot;
for (final key in keys) {
config = config.write(key, defaultConfig.read(key));
_appConfig = _appConfig.write(key, defaultConfig.read(key));
}
snapshot = config;
}
Future<void> write<T, U extends T>(SettingsKey<T> key, U value) async {
if (value == snapshot.read(key)) {
if (value == _appConfig.read(key)) {
return;
}
@@ -80,8 +65,29 @@ class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig
.insertOnConflictUpdate(
SettingsEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
);
snapshot = snapshot.write(key, value);
_appConfig = _appConfig.write(key, value);
}
Stream<AppConfig> watch() => watchSnapshot();
Stream<AppConfig> watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) {
_applyOverrides(rows);
return _appConfig;
});
void _applyOverrides(List<SettingsEntityData> rows) {
_appConfig = AppConfig.fromEntries(
rows.fold({}, (overrides, row) {
final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key);
if (metadataKey == null) {
return overrides;
}
Object? decodedValue;
if (row.value != null) {
decodedValue = metadataKey.decode(row.value!);
}
return {...overrides, metadataKey: decodedValue};
}),
);
}
}
@@ -1,7 +1,9 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
class DriftStoreRepository extends DriftDatabaseRepository {
final Drift _db;
@@ -61,6 +63,8 @@ class DriftStoreRepository extends DriftDatabaseRepository {
const (String) => entity.stringValue,
const (bool) => entity.intValue == 1,
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) =>
entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!),
_ => null,
}
as T?;
@@ -71,6 +75,7 @@ class DriftStoreRepository extends DriftDatabaseRepository {
const (String) => (null, value as String),
const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id),
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
};
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
@@ -17,15 +17,16 @@ class DriftAuthUserRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftAuthUserRepository(super.db) : _db = db;
Selectable<UserDto?> get _authUserQuery => (_db.authUserEntity.select()..limit(1)).asyncMap(_toDto);
Future<UserDto?> get(String id) async {
final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
Future<UserDto?> get() => _authUserQuery.getSingleOrNull();
if (user == null) {
return null;
}
Stream<UserDto?> watch() => _authUserQuery.watchSingleOrNull();
Future<UserDto> _toDto(AuthUserEntityData user) async {
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id));
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(id));
final metadata = await query.map((row) => row.toDto()).get();
return user.toDto(metadata);
}
@@ -5,6 +5,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
@@ -83,7 +85,7 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
final backupSyncManager = ref.read(backgroundSyncProvider);
Future<void> startBackup() async {
final currentUser = ref.read(currentUserProvider);
final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser == null) {
return;
}
@@ -8,15 +8,14 @@ import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
@@ -307,10 +306,9 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
}
void resumeSession() async {
final session = ref.read(sessionProvider);
final serverUrl = session.serverUrl;
final endpoint = session.serverEndpoint;
final accessToken = session.accessToken;
final serverUrl = Store.tryGet(StoreKey.serverUrl);
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
final accessToken = Store.tryGet(StoreKey.accessToken);
if (accessToken != null && serverUrl != null && endpoint != null) {
final infoProvider = ref.read(serverInfoProvider.notifier);
@@ -318,7 +316,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final backgroundManager = ref.read(backgroundSyncProvider);
final backupProvider = ref.read(driftBackupProvider.notifier);
final viewIntentHandler = ref.read(viewIntentHandlerProvider);
final authUserRepository = ref.read(authUserRepositoryProvider);
unawaited(
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
@@ -338,9 +335,9 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
if (syncSuccess) {
await Future.wait([
backgroundManager.hashAssets().then((_) {
_resumeBackup(backupProvider, authUserRepository);
_resumeBackup(backupProvider);
}),
_resumeBackup(backupProvider, authUserRepository),
_resumeBackup(backupProvider),
// TODO: Bring back when the soft freeze issue is addressed
// backgroundManager.syncCloudIds(),
]);
@@ -376,11 +373,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
}
}
Future<void> _resumeBackup(DriftBackupNotifier notifier, DriftAuthUserRepository authUserRepository) async {
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled;
if (isEnableBackup) {
final currentUser = await authUserRepository.get();
final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser != null) {
unawaited(notifier.startForegroundBackup(currentUser.id));
}
@@ -1,8 +1,9 @@
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/services/timeline.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -21,7 +22,7 @@ class OpenInBrowserActionButton extends ConsumerWidget {
});
void _onTap() async {
final serverEndpoint = SessionRepository.instance.session.serverEndpoint!.replaceFirst('/api', '');
final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', '');
String originPath = '';
switch (origin) {
@@ -4,6 +4,8 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
@@ -11,7 +13,6 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
@@ -142,7 +143,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
final remoteId = (videoAsset as RemoteAsset).id;
final serverEndpoint = ref.read(sessionProvider).serverEndpoint!;
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
final String videoUrl = videoAsset.livePhotoVideoId != null
@@ -1,18 +1,18 @@
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/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
class PartnerUserAvatar extends ConsumerWidget {
class PartnerUserAvatar extends StatelessWidget {
const PartnerUserAvatar({super.key, required this.userId, required this.name});
final String userId;
final String name;
@override
Widget build(BuildContext context, WidgetRef ref) {
final url = "${ref.read(sessionProvider).serverEndpoint}/users/$userId/profile-image";
Widget build(BuildContext context) {
final url = "${Store.get(StoreKey.serverEndpoint)}/users/$userId/profile-image";
final nameFirstLetter = name.isNotEmpty ? name[0] : "";
return CircleAvatar(
radius: 16,
@@ -1,7 +1,9 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
@@ -11,7 +13,6 @@ import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:logging/logging.dart';
@@ -139,7 +140,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
final isEnableBackup = _ref.read(appConfigProvider).backup.enabled;
if (isEnableBackup) {
final currentUser = _ref.read(currentUserProvider);
final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser != null) {
await _safeRun(
_ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id),
+5 -7
View File
@@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
@@ -11,7 +10,6 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
@@ -127,10 +125,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
Future<bool> saveAuthInfo({required String accessToken}) async {
await _ref.read(sessionRepository).write(SessionKey.accessToken, accessToken);
await Store.put(StoreKey.accessToken, accessToken);
await _apiService.updateHeaders();
final serverEndpoint = _ref.read(sessionProvider).serverEndpoint!;
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final headerMap = _ref.read(appConfigProvider).network.customHeaders;
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
@@ -138,7 +136,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
// Get the deviceid from the store if it exists, otherwise generate a new one
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
UserDto? user = await _userService.tryGetMyUser();
UserDto? user = _userService.tryGetMyUser();
try {
final serverUser = await _userService.refreshMyUser().timeout(_timeoutDuration);
@@ -195,9 +193,9 @@ class AuthNotifier extends StateNotifier<AuthState> {
return _ref.read(appConfigProvider).network.localEndpoint;
}
/// Returns the current server endpoint (with /api) URL from the session
/// Returns the current server endpoint (with /api) URL from the store
String? getServerEndpoint() {
return _ref.read(sessionProvider).serverEndpoint;
return Store.tryGet(StoreKey.serverEndpoint);
}
Future<String?> setOpenApiServiceEndpoint() {
@@ -1,12 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
final sessionRepository = Provider.autoDispose<SessionRepository>((_) => SessionRepository.instance);
final sessionProvider = Provider.autoDispose<Session>((ref) {
final repo = ref.watch(sessionRepository);
final subscription = repo.watch().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.session;
});
@@ -6,7 +6,7 @@ final settingsProvider = Provider.autoDispose<SettingsRepository>((_) => Setting
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
final repo = ref.watch(settingsProvider);
final subscription = repo.watch().listen((event) => ref.state = event);
final subscription = repo.watchConfig().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.appConfig;
});
@@ -6,18 +6,17 @@ import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
final userRepositoryProvider = Provider((ref) => UserRepository(ref.watch(driftProvider)));
final authUserRepositoryProvider = Provider((ref) => DriftAuthUserRepository(ref.watch(driftProvider)));
final userApiRepositoryProvider = Provider((ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi));
final userServiceProvider = Provider(
(ref) => UserService(
userApiRepository: ref.watch(userApiRepositoryProvider),
authUserRepository: ref.watch(authUserRepositoryProvider),
storeService: ref.watch(storeServiceProvider),
),
);
+1 -1
View File
@@ -7,7 +7,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
class CurrentUserProvider extends StateNotifier<UserDto?> {
CurrentUserProvider(this._userService) : super(null) {
_userService.tryGetMyUser().then((user) => state = user ?? state);
state = _userService.tryGetMyUser();
streamSub = _userService.watchMyUser().listen((user) => state = user ?? state);
}
+3 -2
View File
@@ -1,11 +1,12 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/debounce.dart';
@@ -67,7 +68,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
if (authenticationState.isAuthenticated) {
try {
final endpoint = Uri.parse(_ref.read(sessionProvider).serverEndpoint!);
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
dPrint(() => "Attempting to connect to websocket");
// Configure socket transports must be specified
Socket socket = io(
@@ -5,8 +5,9 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:logging/logging.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@@ -95,7 +96,7 @@ class UploadRepository {
void Function(int bytes, int totalBytes)? onProgress,
required String logContext,
}) async {
final String savedEndpoint = SessionRepository.instance.session.serverEndpoint!;
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
final baseRequest = ProgressMultipartRequest(
'POST',
Uri.parse('$savedEndpoint/assets'),
+10 -6
View File
@@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
@@ -21,8 +23,10 @@ class AuthGuard extends AutoRouteGuard {
// guards, so we keep this function fully sync and validate the token in
// the background — otherwise a slow validateAccessToken() request would
// block the route transition for as long as the OS-level HTTP timeout.
if (SessionRepository.instance.session.accessToken == null) {
_log.warning('No access token in the session.');
try {
Store.get(StoreKey.accessToken);
} on StoreKeyNotFoundException catch (_) {
_log.warning('No access token in the store.');
resolver.next(false);
unawaited(router.replaceAll([const LoginRoute()]));
return;
@@ -36,7 +40,7 @@ class AuthGuard extends AutoRouteGuard {
if (_validateInFlight) {
return;
}
final token = SessionRepository.instance.session.accessToken;
final token = Store.tryGet(StoreKey.accessToken);
if (token == null) {
return;
}
@@ -46,7 +50,7 @@ class AuthGuard extends AutoRouteGuard {
if (res == null || res.authStatus != true) {
// Token may have changed during validation (user logged out + logged in
// again); only act if it still applies to the current session.
if (SessionRepository.instance.session.accessToken != token) {
if (Store.tryGet(StoreKey.accessToken) != token) {
return;
}
_log.fine('User token is invalid. Redirecting to login');
@@ -57,7 +61,7 @@ class AuthGuard extends AutoRouteGuard {
if (e.code != HttpStatus.unauthorized) {
return;
}
if (SessionRepository.instance.session.accessToken != token) {
if (Store.tryGet(StoreKey.accessToken) != token) {
return;
}
_log.warning("Unauthorized access token.");
+5 -5
View File
@@ -3,9 +3,9 @@ import 'dart:convert';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/url_helper.dart';
@@ -41,7 +41,7 @@ class ApiService {
// The below line ensures that the api clients are initialized when the service is instantiated
// This is required to avoid late initialization errors when the clients are access before the endpoint is resolved
setEndpoint('');
final endpoint = SessionRepository.instance.session.serverEndpoint;
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
if (endpoint != null && endpoint.isNotEmpty) {
setEndpoint(endpoint);
}
@@ -84,7 +84,7 @@ class ApiService {
setEndpoint(endpoint);
// Save in local database for next startup
await SessionRepository.instance.write(SessionKey.serverEndpoint, endpoint);
await Store.put(StoreKey.serverEndpoint, endpoint);
return endpoint;
}
@@ -173,7 +173,7 @@ class ApiService {
static List<String> getServerUrls() {
final urls = <String>[];
final serverEndpoint = SessionRepository.instance.session.serverEndpoint;
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint);
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint);
}
+6 -5
View File
@@ -1,12 +1,12 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@@ -55,7 +55,7 @@ class AuthService {
Future<String> validateServerUrl(String url) async {
final validUrl = await _apiService.resolveAndSetEndpoint(url);
await _apiService.setDeviceInfoHeader();
await SessionRepository.instance.write(SessionKey.serverUrl, validUrl);
await Store.put(StoreKey.serverUrl, validUrl);
return validUrl;
}
@@ -118,7 +118,8 @@ class AuthService {
await _backgroundSyncManager.cancel();
await Future.wait([
_authRepository.clearLocalData(),
SessionRepository.instance.clear([SessionKey.accessToken]),
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
SettingsRepository.instance.clear(const [
.networkAutoEndpointSwitching,
.networkPreferredWifiName,
@@ -13,7 +13,6 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
@@ -387,7 +386,7 @@ class BackgroundUploadService {
String? latitude,
String? longitude,
}) async {
final serverEndpoint = SessionRepository.instance.session.serverEndpoint!;
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final url = Uri.parse('$serverEndpoint/assets').toString();
final headers = ApiService.getRequestHeaders();
final deviceId = Store.get(StoreKey.deviceId);
-3
View File
@@ -6,7 +6,6 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
@@ -52,8 +51,6 @@ abstract final class Bootstrap {
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
await SessionRepository.ensureInitialized(drift);
final settingsRepo = await SettingsRepository.ensureInitialized(drift);
await LogService.init(
+6 -6
View File
@@ -1,8 +1,9 @@
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:openapi/api.dart';
String getOriginalUrlForRemoteId(final String id, {bool edited = true}) {
return '${SessionRepository.instance.session.serverEndpoint!}/assets/$id/original?edited=$edited';
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited';
}
String getThumbnailUrlForRemoteId(
@@ -11,15 +12,14 @@ String getThumbnailUrlForRemoteId(
bool edited = true,
String? thumbhash,
}) {
final url =
'${SessionRepository.instance.session.serverEndpoint!}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url;
}
String getPlaybackUrlForRemoteId(final String id) {
return '${SessionRepository.instance.session.serverEndpoint!}/assets/$id/video/playback?';
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
}
String getFaceThumbnailUrl(final String personId) {
return '${SessionRepository.instance.session.serverEndpoint!}/people/$personId/thumbnail';
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
}
+35 -69
View File
@@ -8,20 +8,17 @@ import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
const int targetVersion = 27;
const int targetVersion = 26;
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
final int version = Store.get(StoreKey.version, targetVersion);
@@ -34,22 +31,18 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
await _migrateTo26(drift);
}
if (version < 27) {
await _migrateTo27(drift);
}
await Store.put(StoreKey.version, targetVersion);
return;
}
Future<void> _migrateTo25() async {
final accessToken = Store.tryGet(StoreKey.legacyAccessToken);
final accessToken = Store.tryGet(StoreKey.accessToken);
if (accessToken == null || accessToken.isEmpty) {
return;
}
final urls = <String>[];
final serverEndpoint = Store.tryGet(StoreKey.legacyServerEndpoint);
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint);
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint);
}
@@ -80,7 +73,7 @@ Future<void> _migrateTo25() async {
}
Future<void> _migrateTo26(Drift drift) async {
final migrator = _StoreMigrator.settings(drift);
final migrator = _StoreMigrator(drift);
await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, SettingsKey.logLevel, LogLevel.values);
// Theme
await migrator.migrateEnumName(StoreKey.legacyThemeMode, SettingsKey.themeMode, ThemeMode.values);
@@ -145,17 +138,7 @@ Future<void> _migrateTo26(Drift drift) async {
await migrator.complete();
}
Future<void> _migrateTo27(Drift drift) async {
final migrator = _StoreMigrator.session(drift);
await migrator.migrateString(StoreKey.legacyServerUrl, SessionKey.serverUrl);
await migrator.migrateString(StoreKey.legacyAccessToken, SessionKey.accessToken);
await migrator.migrateString(StoreKey.legacyServerEndpoint, SessionKey.serverEndpoint);
await migrator.complete();
await SessionRepository.instance.refresh();
}
Future<void> _migrateAlbumSortMode(_StoreMigrator<SettingsKey> migrator) async {
Future<void> _migrateAlbumSortMode(_StoreMigrator migrator) async {
final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id);
final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw);
if (mode == null) {
@@ -165,7 +148,7 @@ Future<void> _migrateAlbumSortMode(_StoreMigrator<SettingsKey> migrator) async {
migrator.stage(StoreKey.legacySelectedAlbumSortOrder, SettingsKey.albumSortMode, mode);
}
Future<void> _migrateExternalEndpointList(_StoreMigrator<SettingsKey> migrator) async {
Future<void> _migrateExternalEndpointList(_StoreMigrator migrator) async {
final raw = await migrator.readLegacyStoreString(StoreKey.legacyExternalEndpointList.id);
if (raw == null) {
return;
@@ -189,7 +172,7 @@ Future<void> _migrateExternalEndpointList(_StoreMigrator<SettingsKey> migrator)
migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls);
}
Future<void> _migrateCustomHeaders(_StoreMigrator<SettingsKey> migrator) async {
Future<void> _migrateCustomHeaders(_StoreMigrator migrator) async {
final raw = await migrator.readLegacyStoreString(StoreKey.legacyCustomHeaders.id);
if (raw == null) {
return;
@@ -212,39 +195,14 @@ Future<void> _migrateCustomHeaders(_StoreMigrator<SettingsKey> migrator) async {
migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers);
}
class _StoreMigrator<K extends Enum> {
_StoreMigrator._(this._db, {required this.encode, required this.readDefault, required this.insertRow});
static _StoreMigrator<SettingsKey> settings(Drift db) => _StoreMigrator<SettingsKey>._(
db,
encode: (key, value) => key.encode(value),
readDefault: (key) => defaultConfig.read(key),
insertRow: (batch, name, value) => batch.insert(
db.settingsEntity,
SettingsEntityCompanion(key: Value(name), value: Value(value)),
mode: InsertMode.insertOrReplace,
),
);
static _StoreMigrator<SessionKey> session(Drift db) => _StoreMigrator<SessionKey>._(
db,
encode: (key, value) => key.encode(value),
readDefault: (key) => defaultSession.read(key),
insertRow: (batch, name, value) => batch.insert(
db.sessionEntity,
SessionEntityCompanion(key: Value(name), value: Value(value)),
mode: InsertMode.insertOrReplace,
),
);
class _StoreMigrator {
final Drift _db;
final String Function(K key, Object value) encode;
final Object? Function(K key) readDefault;
final void Function(Batch batch, String name, String? value) insertRow;
final Map<K, Object?> _cache = {};
final Map<SettingsKey, Object?> _cache = {};
final List<int> _migratedStoreIds = [];
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, K newKey, List<T> values) async {
_StoreMigrator(this._db);
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, SettingsKey<T> newKey, List<T> values) async {
final index = await readLegacyStoreInt(legacyKey.id);
if (index == null) {
return;
@@ -259,7 +217,11 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateEnumName<T extends Enum>(StoreKey<String> legacyKey, K newKey, List<T> values) async {
Future<void> migrateEnumName<T extends Enum>(
StoreKey<String> legacyKey,
SettingsKey<T> newKey,
List<T> values,
) async {
final name = await readLegacyStoreString(legacyKey.id);
if (name == null) {
return;
@@ -274,17 +236,18 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateBool(StoreKey<bool> legacyKey, K newKey) async {
Future<void> migrateBool(StoreKey<bool> legacyKey, SettingsKey<bool> newKey) async {
final intValue = await readLegacyStoreInt(legacyKey.id);
if (intValue == null) {
return;
}
_cache[newKey] = intValue != 0;
final boolValue = intValue != 0;
_cache[newKey] = boolValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateInt(StoreKey<int> legacyKey, K newKey) async {
Future<void> migrateInt(StoreKey<int> legacyKey, SettingsKey<int> newKey) async {
final intValue = await readLegacyStoreInt(legacyKey.id);
if (intValue == null) {
return;
@@ -294,9 +257,9 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateString(StoreKey<String> legacyKey, K newKey) async {
Future<void> migrateString(StoreKey<String> legacyKey, SettingsKey<String> newKey) async {
final value = await readLegacyStoreString(legacyKey.id);
if (value == null || value.isEmpty) {
if (value == null) {
return;
}
@@ -304,12 +267,7 @@ class _StoreMigrator<K extends Enum> {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateNullableString(StoreKey<String> legacyKey, K newKey) async {
_cache[newKey] = await readLegacyStoreString(legacyKey.id);
_migratedStoreIds.add(legacyKey.id);
}
void stage(StoreKey legacyKey, K newKey, Object? value) {
void stage<T, U extends T>(StoreKey legacyKey, SettingsKey<T> newKey, U value) {
_cache[newKey] = value;
_migratedStoreIds.add(legacyKey.id);
}
@@ -317,12 +275,20 @@ class _StoreMigrator<K extends Enum> {
Future<void> complete() async {
await _db.batch((batch) {
for (final entry in _cache.entries) {
if (entry.value == readDefault(entry.key)) {
if (entry.value == defaultConfig.read(entry.key)) {
continue;
}
final value = entry.value;
insertRow(batch, entry.key.name, value == null ? null : encode(entry.key, value));
String? resolvedValue;
if (entry.value != null) {
resolvedValue = entry.key.encode(entry.value);
}
batch.insert(
_db.settingsEntity,
SettingsEntityCompanion(key: Value(entry.key.name), value: Value(resolvedValue)),
mode: InsertMode.insertOrReplace,
);
}
});
await deleteLegacyStoreRows(_migratedStoreIds);
+3 -2
View File
@@ -1,4 +1,5 @@
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:punycode/punycode.dart';
String sanitizeUrl(String url) {
@@ -10,7 +11,7 @@ String sanitizeUrl(String url) {
}
String? getServerUrl() {
final serverUrl = punycodeDecodeUrl(SessionRepository.instance.session.serverEndpoint);
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint));
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
if (serverUri == null) {
return null;
+1 -5
View File
@@ -1,8 +1,4 @@
String? getVersionCompatibilityMessage(int appMajor, int appMinor, int serverMajor, int serverMinor) {
if (serverMajor != appMajor) {
return 'Your app major version is not compatible with the server!';
}
String? getVersionCompatibilityMessage(int _, int appMinor, int _, int serverMinor) {
// Add latest compat info up top
if (serverMinor < 106 && appMinor >= 106) {
return 'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login';
+3 -2
View File
@@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
final url = "${SessionRepository.instance.session.serverEndpoint!}/users/${u.id}/profile-image";
final url = "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image";
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
return CircleAvatar(
radius: radius,
@@ -1,8 +1,9 @@
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/entities/store.entity.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
// ignore: must_be_immutable
class UserCircleAvatar extends ConsumerWidget {
@@ -17,7 +18,7 @@ class UserCircleAvatar extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final userAvatarColor = user.avatarColor.toColor().withValues(alpha: opacity);
final profileImageUrl =
'${ref.read(sessionProvider).serverEndpoint}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}';
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}';
final textColor = (user.avatarColor.toColor().computeLuminance() > 0.5 ? Colors.black : Colors.white).withValues(
alpha: opacity,
@@ -21,13 +21,13 @@ void main() {
controller = StreamController<List<StoreDto<Object>>>.broadcast();
mockDriftStoreRepo = MockDriftStoreRepository();
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
registerFallbackValue(StoreKey.legacyAccessToken);
registerFallbackValue(StoreKey.accessToken);
registerFallbackValue(StoreKey.version);
registerFallbackValue(StoreKey.advancedTroubleshooting);
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
(_) async => [
const StoreDto(StoreKey.legacyAccessToken, _kAccessToken),
const StoreDto(StoreKey.accessToken, _kAccessToken),
const StoreDto(StoreKey.advancedTroubleshooting, _kAdvancedTroubleshooting),
const StoreDto(StoreKey.version, _kVersion),
],
@@ -45,35 +45,35 @@ void main() {
group("Store Service Init:", () {
test('Populates the internal cache on init', () {
verify(() => mockDriftStoreRepo.getAll()).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.advancedTroubleshooting), _kAdvancedTroubleshooting);
expect(sut.tryGet(StoreKey.version), _kVersion);
// Other keys should be null
expect(sut.tryGet(StoreKey.deviceId), isNull);
expect(sut.tryGet(StoreKey.currentUser), isNull);
});
test('Listens to stream of store updates', () async {
final event = StoreDto(StoreKey.legacyAccessToken, _kAccessToken.toUpperCase());
final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase());
controller.add([event]);
await pumpEventQueue();
verify(() => mockDriftStoreRepo.watchAll()).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken.toUpperCase());
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase());
});
});
group('Store Service get:', () {
test('Returns the stored value for the given key', () {
expect(sut.get(StoreKey.legacyAccessToken), _kAccessToken);
expect(sut.get(StoreKey.accessToken), _kAccessToken);
});
test('Throws StoreKeyNotFoundException for nonexistent keys', () {
expect(() => sut.get(StoreKey.deviceId), throwsA(isA<StoreKeyNotFoundException>()));
expect(() => sut.get(StoreKey.currentUser), throwsA(isA<StoreKeyNotFoundException>()));
});
test('Returns the stored value for the given key or the defaultValue', () {
expect(sut.get(StoreKey.legacyBackupTriggerDelay, 5), 5);
expect(sut.get(StoreKey.currentUser, 5), 5);
});
});
@@ -83,15 +83,15 @@ void main() {
});
test('Skip insert when value is not modified', () async {
await sut.put(StoreKey.legacyAccessToken, _kAccessToken);
verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, any()));
await sut.put(StoreKey.accessToken, _kAccessToken);
verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, any()));
});
test('Insert value when modified', () async {
final newAccessToken = _kAccessToken.toUpperCase();
await sut.put(StoreKey.legacyAccessToken, newAccessToken);
verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, newAccessToken)).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), newAccessToken);
await sut.put(StoreKey.accessToken, newAccessToken);
verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1);
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
});
});
@@ -108,7 +108,7 @@ void main() {
});
test('Watches a specific key for changes', () async {
final stream = sut.watch(StoreKey.legacyAccessToken);
final stream = sut.watch(StoreKey.accessToken);
final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()];
unawaited(expectLater(stream, emitsInOrder(events)));
@@ -118,7 +118,7 @@ void main() {
}
await pumpEventQueue();
verify(() => mockDriftStoreRepo.watch<String>(StoreKey.legacyAccessToken)).called(1);
verify(() => mockDriftStoreRepo.watch<String>(StoreKey.accessToken)).called(1);
});
});
@@ -128,13 +128,13 @@ void main() {
});
test('Removes the value from the DB', () async {
await sut.delete(StoreKey.legacyAccessToken);
verify(() => mockDriftStoreRepo.delete<String>(StoreKey.legacyAccessToken)).called(1);
await sut.delete(StoreKey.accessToken);
verify(() => mockDriftStoreRepo.delete<String>(StoreKey.accessToken)).called(1);
});
test('Removes the value from the cache', () async {
await sut.delete(StoreKey.legacyAccessToken);
expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
await sut.delete(StoreKey.accessToken);
expect(sut.tryGet(StoreKey.accessToken), isNull);
});
});
@@ -146,7 +146,7 @@ void main() {
test('Clears all values from the store', () async {
await sut.clear();
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
expect(sut.tryGet(StoreKey.accessToken), isNull);
expect(sut.tryGet(StoreKey.advancedTroubleshooting), isNull);
expect(sut.tryGet(StoreKey.version), isNull);
});
@@ -1,62 +1,78 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:mocktail/mocktail.dart';
import '../../fixtures/user.stub.dart';
import '../../infrastructure/repository.mock.dart';
import '../service.mock.dart';
void main() {
late UserService sut;
late UserApiRepository mockUserApiRepo;
late DriftAuthUserRepository mockAuthUserRepo;
late StoreService mockStoreService;
setUp(() {
mockUserApiRepo = MockUserApiRepository();
mockAuthUserRepo = MockDriftAuthUserRepository();
sut = UserService(userApiRepository: mockUserApiRepo, authUserRepository: mockAuthUserRepo);
mockStoreService = MockStoreService();
sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService);
registerFallbackValue(UserStub.admin);
when(() => mockAuthUserRepo.get()).thenAnswer((_) async => UserStub.admin);
when(() => mockAuthUserRepo.upsert(any())).thenAnswer((_) async => UserStub.admin);
when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin);
when(() => mockStoreService.tryGet(StoreKey.currentUser)).thenReturn(UserStub.admin);
});
group('tryGetMyUser', () {
test('should return the current user from the auth user repository', () async {
final result = await sut.tryGetMyUser();
group('getMyUser', () {
test('should return user from store', () {
final result = sut.getMyUser();
expect(result, UserStub.admin);
});
test('should return null if no user is logged in', () async {
when(() => mockAuthUserRepo.get()).thenAnswer((_) async => null);
final result = await sut.tryGetMyUser();
test('should handle user not found scenario', () {
when(() => mockStoreService.get(StoreKey.currentUser)).thenThrow(Exception('User not found'));
expect(() => sut.getMyUser(), throwsA(isA<Exception>()));
});
});
group('tryGetMyUser', () {
test('should return user from store', () {
final result = sut.tryGetMyUser();
expect(result, UserStub.admin);
});
test('should return null if user not found', () {
when(() => mockStoreService.tryGet(StoreKey.currentUser)).thenReturn(null);
final result = sut.tryGetMyUser();
expect(result, isNull);
});
});
group('watchMyUser', () {
test('should return the current user stream from the auth user repository', () {
when(() => mockAuthUserRepo.watch()).thenAnswer((_) => Stream.value(UserStub.admin));
test('should return user stream from store', () {
when(() => mockStoreService.watch(StoreKey.currentUser)).thenAnswer((_) => Stream.value(UserStub.admin));
final result = sut.watchMyUser();
expect(result, emits(UserStub.admin));
});
test('should return an empty stream if no user is logged in', () {
when(() => mockAuthUserRepo.watch()).thenAnswer((_) => const Stream.empty());
test('should return an empty stream if user not found', () {
when(() => mockStoreService.watch(StoreKey.currentUser)).thenAnswer((_) => const Stream.empty());
final result = sut.watchMyUser();
expect(result, emitsInOrder([]));
});
});
group('refreshMyUser', () {
test('should return user from api and persist it', () async {
test('should return user from api and store it', () async {
when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin);
when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true);
final result = await sut.refreshMyUser();
verify(() => mockAuthUserRepo.upsert(UserStub.admin)).called(1);
verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1);
expect(result, UserStub.admin);
});
@@ -64,7 +80,7 @@ void main() {
when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => null);
final result = await sut.refreshMyUser();
verifyNever(() => mockAuthUserRepo.upsert(any()));
verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin));
expect(result, isNull);
});
});
@@ -72,26 +88,29 @@ void main() {
group('createProfileImage', () {
test('should return profile image path', () async {
const profileImagePath = 'profile.jpg';
final updatedUser = UserStub.admin;
when(
() => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)),
).thenAnswer((_) async => profileImagePath);
when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true);
final result = await sut.createProfileImage(profileImagePath, Uint8List(0));
verify(() => mockAuthUserRepo.upsert(UserStub.admin)).called(1);
verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1);
expect(result, profileImagePath);
});
test('should return null if profile image creation fails', () async {
const profileImagePath = 'profile.jpg';
final updatedUser = UserStub.admin;
when(
() => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)),
).thenThrow(Exception('Failed to create profile image'));
final result = await sut.createProfileImage(profileImagePath, Uint8List(0));
verifyNever(() => mockAuthUserRepo.upsert(any()));
verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser));
expect(result, isNull);
});
});
-4
View File
@@ -34,7 +34,6 @@ import 'schema_v27.dart' as v27;
import 'schema_v28.dart' as v28;
import 'schema_v29.dart' as v29;
import 'schema_v30.dart' as v30;
import 'schema_v31.dart' as v31;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@@ -100,8 +99,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v29.DatabaseAtV29(db);
case 30:
return v30.DatabaseAtV30(db);
case 31:
return v31.DatabaseAtV31(db);
default:
throw MissingSchemaException(version, versions);
}
@@ -138,6 +135,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
28,
29,
30,
31,
];
}
File diff suppressed because it is too large Load Diff
@@ -4,13 +4,17 @@ import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import '../../fixtures/user.stub.dart';
const _kTestAccessToken = "#TestToken";
const _kTestVersion = 10;
const _kTestAdvancedTroubleshooting = false;
final _kTestUser = UserStub.admin;
Future<void> _populateStore(Drift db) async {
await db.batch((batch) async {
@@ -25,7 +29,7 @@ Future<void> _populateStore(Drift db) async {
batch.insert(
db.storeEntity,
StoreEntityCompanion(
id: Value(StoreKey.legacyAccessToken.id),
id: Value(StoreKey.accessToken.id),
intValue: const Value(null),
stringValue: const Value(_kTestAccessToken),
),
@@ -64,10 +68,10 @@ void main() {
});
test('converts string', () async {
String? accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
String? accessToken = await sut.tryGet(StoreKey.accessToken);
expect(accessToken, isNull);
await sut.upsert(StoreKey.legacyAccessToken, _kTestAccessToken);
accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
await sut.upsert(StoreKey.accessToken, _kTestAccessToken);
accessToken = await sut.tryGet(StoreKey.accessToken);
expect(accessToken, _kTestAccessToken);
});
@@ -78,6 +82,14 @@ void main() {
advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting);
});
test('converts user', () async {
UserDto? user = await sut.tryGet(StoreKey.currentUser);
expect(user, isNull);
await sut.upsert(StoreKey.currentUser, _kTestUser);
user = await sut.tryGet(StoreKey.currentUser);
expect(user, _kTestUser);
});
});
group('Store Repository Deletes:', () {
@@ -135,12 +147,12 @@ void main() {
emitsInOrder([
[
const StoreDto<Object>(StoreKey.version, _kTestVersion),
const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
],
[
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
],
]),
@@ -48,8 +48,6 @@ class MockSyncMigrationRepository extends Mock implements SyncMigrationRepositor
class MockUserRepository extends Mock implements UserRepository {}
class MockDriftAuthUserRepository extends Mock implements DriftAuthUserRepository {}
class MockPartnerRepository extends Mock implements PartnerRepository {}
// API Repos
@@ -44,6 +44,94 @@ void main() {
});
});
group('getAll', () {
test('returns album when all of its assets are trashed', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final asset1 = await ctx.newRemoteAsset(ownerId: user.id, deletedAt: DateTime(2025, 1, 1));
final asset2 = await ctx.newRemoteAsset(ownerId: user.id, deletedAt: DateTime(2025, 1, 1));
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset1.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset2.id);
final albums = await sut.getAll();
expect(albums, hasLength(1));
expect(albums.first.id, album.id);
expect(albums.first.assetCount, 0);
});
test('excludes trashed assets from assetCount', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final active1 = await ctx.newRemoteAsset(ownerId: user.id);
final active2 = await ctx.newRemoteAsset(ownerId: user.id);
final trashed = await ctx.newRemoteAsset(ownerId: user.id, deletedAt: DateTime(2025, 1, 1));
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active1.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active2.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: trashed.id);
final albums = await sut.getAll();
expect(albums, hasLength(1));
expect(albums.first.assetCount, 2);
});
test('returns album without assets', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final albums = await sut.getAll();
expect(albums, hasLength(1));
expect(albums.first.id, album.id);
expect(albums.first.assetCount, 0);
});
});
group('get', () {
test('returns the album when all of its assets are trashed', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final asset = await ctx.newRemoteAsset(ownerId: user.id, deletedAt: DateTime(2025, 1, 1));
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset.id);
final result = await sut.get(album.id);
expect(result, isNotNull);
expect(result?.id, album.id);
expect(result?.assetCount, 0);
});
});
group('getAlbumsContainingAsset', () {
test('excludes trashed assets from assetCount', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final asset = await ctx.newRemoteAsset(ownerId: user.id);
final trashed = await ctx.newRemoteAsset(ownerId: user.id, deletedAt: DateTime(2025, 1, 1));
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: trashed.id);
final albums = await sut.getAlbumsContainingAsset(asset.id);
expect(albums, hasLength(1));
expect(albums.first.id, album.id);
expect(albums.first.assetCount, 1);
});
test('returns albums for a trashed asset', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final trashed = await ctx.newRemoteAsset(ownerId: user.id, deletedAt: DateTime(2025, 1, 1));
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: trashed.id);
final albums = await sut.getAlbumsContainingAsset(trashed.id);
expect(albums, hasLength(1));
expect(albums.first.assetCount, 0);
});
});
group('getSortedAlbumIds', () {
late String userId;
@@ -1,118 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import '../repository_context.dart';
void main() {
late MediumRepositoryContext ctx;
late SessionRepository sut;
setUpAll(() async {
ctx = MediumRepositoryContext();
sut = await SessionRepository.ensureInitialized(ctx.db);
});
tearDownAll(() async {
await ctx.dispose();
});
setUp(() async {
await ctx.db.delete(ctx.db.sessionEntity).go();
await SessionRepository.instance.refresh();
});
group('defaults', () {
test('session returns null fields when DB is empty', () {
expect(sut.session.serverUrl, isNull);
expect(sut.session.accessToken, isNull);
expect(sut.session.serverEndpoint, isNull);
});
});
group('write', () {
test('persists a value and reflects it in the composed view', () async {
await sut.write(.serverEndpoint, 'https://demo.immich.app/api');
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
});
test('persists across keys independently', () async {
await sut.write(.serverUrl, 'https://demo.immich.app');
await sut.write(.accessToken, 'token-123');
expect(sut.session.serverUrl, 'https://demo.immich.app');
expect(sut.session.accessToken, 'token-123');
expect(sut.session.serverEndpoint, isNull);
});
});
group('null values', () {
test('a stored NULL value column decodes to null on refresh', () async {
await ctx.db
.into(ctx.db.sessionEntity)
.insert(
SessionEntityCompanion.insert(
key: SessionKey.accessToken.name,
value: const .new(null),
updatedAt: .new(DateTime.now()),
),
);
await SessionRepository.instance.refresh();
expect(sut.session.accessToken, isNull);
});
});
group('sync', () {
test('picks up rows that were inserted directly into the DB', () async {
await ctx.db
.into(ctx.db.sessionEntity)
.insert(
SessionEntityCompanion.insert(
key: SessionKey.serverEndpoint.name,
value: const .new('https://demo.immich.app/api'),
updatedAt: .new(DateTime.now()),
),
);
expect(sut.session.serverEndpoint, isNull);
await SessionRepository.instance.refresh();
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
});
test('drops cached values for rows that were deleted out from under the repo', () async {
await sut.write(.serverEndpoint, 'https://demo.immich.app/api');
await ctx.db.delete(ctx.db.sessionEntity).go();
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
await SessionRepository.instance.refresh();
expect(sut.session.serverEndpoint, isNull);
});
test('skips rows whose key is unknown to SessionKey', () async {
await ctx.db
.into(ctx.db.sessionEntity)
.insert(
SessionEntityCompanion.insert(
key: 'session.unknown.future-key',
value: const .new('unknown'),
updatedAt: .new(DateTime.now()),
),
);
await SessionRepository.instance.refresh();
expect(sut.session.serverEndpoint, isNull);
});
});
group('watch', () {
test('watchSession emits the new value after a write', () async {
final expectation = expectLater(
sut.watch().map((s) => s.serverEndpoint),
emitsThrough('https://demo.immich.app/api/watch'),
);
await sut.write(SessionKey.serverEndpoint, 'https://demo.immich.app/api/watch');
await expectation;
});
});
}
@@ -142,13 +142,13 @@ void main() {
group('watch', () {
test('watchAppConfig emits the new value after a write', () async {
final expectation = expectLater(sut.watch().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark));
final expectation = expectLater(sut.watchConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark));
await sut.write(SettingsKey.themeMode, ThemeMode.dark);
await expectation;
});
test('watchConfig emits the new value after a write', () async {
final expectation = expectLater(sut.watch().map((c) => c.logLevel), emitsThrough(LogLevel.warning));
final expectation = expectLater(sut.watchConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning));
await sut.write(SettingsKey.logLevel, LogLevel.warning);
await expectation;
});
@@ -5,9 +5,6 @@ void main() {
test('getVersionCompatibilityMessage', () {
String? result;
result = getVersionCompatibilityMessage(1, 0, 2, 0);
expect(result, 'Your app major version is not compatible with the server!');
result = getVersionCompatibilityMessage(1, 106, 1, 105);
expect(
result,
@@ -60,7 +60,7 @@ void main() {
when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => true);
when(() => assetService.watchAsset(any())).thenAnswer((_) => const Stream.empty());
when(() => assetService.getExif(any())).thenAnswer((_) async => null);
when(() => userService.tryGetMyUser()).thenAnswer((_) async => _user);
when(() => userService.tryGetMyUser()).thenReturn(_user);
when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty());
container = ProviderContainer(
@@ -4,7 +4,6 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/services/auth.service.dart';
@@ -45,7 +44,6 @@ void main() {
WidgetsFlutterBinding.ensureInitialized();
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await SessionRepository.ensureInitialized(db);
});
tearDownAll(() async {
@@ -7,12 +7,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/services/background_upload.service.dart';
@@ -41,8 +39,8 @@ void main() {
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db));
await SettingsRepository.ensureInitialized(db);
await SessionRepository.ensureInitialized(db);
await SessionRepository.instance.write(SessionKey.serverEndpoint, 'https://demo.immich.app');
await Store.put(StoreKey.serverEndpoint, 'http://test-server.com');
await Store.put(StoreKey.deviceId, 'test-device-id');
});
+2 -4
View File
@@ -5,11 +5,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import '../test_utils.dart';
@@ -26,8 +25,7 @@ class PresentationContext {
if (_db == null) {
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false);
await SessionRepository.ensureInitialized(db);
await SessionRepository.instance.write(SessionKey.serverEndpoint, serverEndpoint);
await StoreService.I.put(StoreKey.serverEndpoint, serverEndpoint);
_db = db;
}
return const PresentationContext._();
@@ -1,99 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/value_codec.dart';
enum _Fruit { apple, banana, cherry }
void main() {
group('MapCodec', () {
group('encode', () {
test('serializes an empty map to an empty JSON object', () {
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
expect(codec.encode({}), '{}');
});
test('encodes a string-to-string map as a JSON object', () {
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
expect(codec.encode({'a': '1', 'b': '2'}), '{"a":"1","b":"2"}');
});
test('stringifies non-string values via the value codec', () {
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
expect(codec.encode({'x': 10, 'y': 20}), '{"x":"10","y":"20"}');
});
test('stringifies non-string keys via the key codec', () {
const codec = MapCodec<int, String>(PrimitiveCodec.integer, PrimitiveCodec.string);
expect(codec.encode({1: 'one', 2: 'two'}), '{"1":"one","2":"two"}');
});
});
group('decode', () {
test('reconstructs a string-to-string map', () {
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
expect(codec.decode('{"a":"1","b":"2"}'), {'a': '1', 'b': '2'});
});
test('parses values back to their domain type', () {
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
expect(codec.decode('{"x":"10","y":"20"}'), {'x': 10, 'y': 20});
});
test('parses keys back to their domain type', () {
const codec = MapCodec<int, String>(PrimitiveCodec.integer, PrimitiveCodec.string);
expect(codec.decode('{"1":"one","2":"two"}'), {1: 'one', 2: 'two'});
});
test('returns an empty map for an empty JSON object', () {
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
expect(codec.decode('{}'), isEmpty);
});
test('returns an empty map when the payload is not valid JSON', () {
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
expect(codec.decode('not json'), isEmpty);
});
test('returns an empty map when the JSON root is not an object', () {
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
expect(codec.decode('[]'), isEmpty);
expect(codec.decode('"a string"'), isEmpty);
expect(codec.decode('42'), isEmpty);
});
test('skips entries whose value is not a JSON string, keeping the rest', () {
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
expect(codec.decode('{"x":1,"y":"20"}'), {'y': 20});
});
test('skips entries whose value is a nested object, keeping the rest', () {
const codec = MapCodec<String, String>(PrimitiveCodec.string, PrimitiveCodec.string);
expect(codec.decode('{"a":{"nested":"value"},"b":"ok"}'), {'b': 'ok'});
});
test('returns an empty map when every entry is malformed', () {
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
expect(codec.decode('{"x":1,"y":2}'), isEmpty);
});
});
group('round trip', () {
test('preserves a primitive map through encode then decode', () {
const codec = MapCodec<String, int>(PrimitiveCodec.string, PrimitiveCodec.integer);
const original = {'one': 1, 'two': 2, 'three': 3};
expect(codec.decode(codec.encode(original)), original);
});
test('preserves an enum-valued map by composing with EnumCodec', () {
const codec = MapCodec<String, _Fruit>(PrimitiveCodec.string, EnumCodec(_Fruit.values));
const original = {'breakfast': _Fruit.banana, 'snack': _Fruit.apple};
expect(codec.decode(codec.encode(original)), original);
});
test('preserves a DateTime-valued map by composing with DateTimeCodec', () {
const codec = MapCodec<String, DateTime>(PrimitiveCodec.string, DateTimeCodec());
final original = {'created': DateTime.utc(2024, 1, 1, 12, 30)};
expect(codec.decode(codec.encode(original)), original);
});
});
});
}
+1 -1
View File
@@ -11,7 +11,7 @@
"release": "./misc/release/pump-version.sh",
"pump": "node ./misc/release/pump-wrapper.js"
},
"packageManager": "pnpm@11.4.0",
"packageManager": "pnpm@11.5.2",
"engines": {
"pnpm": ">=10.0.0"
},
+1 -1
View File
@@ -29,7 +29,7 @@
"@vitest/coverage-v8": "^4.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"commander": "^15.0.0",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
+1 -1
View File
@@ -13,5 +13,5 @@
"oidc-provider": "^9.0.0",
"tsx": "^4.20.6"
},
"packageManager": "pnpm@11.4.0"
"packageManager": "pnpm@11.5.2"
}
+1 -1
View File
@@ -24,7 +24,7 @@
"keywords": [],
"author": "",
"license": "GNU Affero General Public License version 3",
"packageManager": "pnpm@11.4.0",
"packageManager": "pnpm@11.5.2",
"devDependencies": {
"@extism/js-pdk": "^1.1.1",
"@immich/sdk": "workspace:*",
+1230 -1135
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -16,6 +16,7 @@ import {
ToneMappingSchema,
TranscodeHardwareAccelerationSchema,
TranscodePolicySchema,
VideoCodec,
VideoCodecSchema,
VideoContainerSchema,
} from 'src/enum';
@@ -109,6 +110,9 @@ const SystemConfigFFmpegSchema = z
})
.meta({ id: 'SystemConfigFFmpegRealtimeDto' }),
})
.refine((ffmpeg) => (ffmpeg.targetVideoCodec === VideoCodec.Av1 ? ffmpeg.threads <= 6 : true), {
error: 'Threads: for AV1, parallelism must be 06',
})
.meta({ id: 'SystemConfigFFmpegDto' });
const SystemConfigJobSchema = z
@@ -456,6 +456,11 @@ describe(SystemConfigService.name, () => {
},
{ should: 'warn for top level unknown options', warn: true, config: { unknownOption: true } },
{ should: 'warn for nested unknown options', warn: true, config: { ffmpeg: { unknownOption: true } } },
{
should: 'validate av1 parallelism setting',
config: { ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 7 } },
throws: '[ffmpeg] Threads: for AV1, parallelism must be 06',
},
];
for (const test of tests) {
+1 -1
View File
@@ -105,7 +105,7 @@
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^4.0.0",
"rollup-plugin-visualizer": "^7.0.0",
"svelte": "5.55.8",
"svelte": "5.56.2",
"svelte-check": "^4.4.6",
"svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.2.4",
+34 -43
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { locale } from '$lib/stores/preferences.store';
import type { CalendarHeatmapResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon';
import { Text } from '@immich/ui';
import { DateTime, Info } from 'luxon';
import { t } from 'svelte-i18n';
type Props = {
@@ -12,20 +13,8 @@
const { data, itemLabel, totalLabel }: Props = $props();
const { rows } = $derived.by(() => {
const weeks = Array.from({ length: Math.ceil(data.series.length / 7) }, (_, index) =>
data.series.slice(index * 7, index * 7 + 7),
);
const rows = Array.from({ length: 7 }, (_, dayIndex) => weeks.map((week) => week[dayIndex]).filter(Boolean));
const endDate = DateTime.fromISO(data.to, { zone: 'utc' });
const months = Array.from({ length: 4 }, (_, index) =>
endDate.minus({ months: 11 - index * 4 }).toLocaleString({ month: 'short' }, { locale: $locale }),
);
return { rows, months };
});
const startDate = $derived(DateTime.fromISO(data.from, { zone: 'utc' }));
const padding = $derived(startDate.diff(startDate.startOf('week', { useLocaleWeeks: true })).as('days'));
const maxCount = $derived(Math.max(...data.series.map((item) => item.count), 0));
@@ -49,45 +38,47 @@
return 'bg-immich-primary';
};
// const dayLabels = $derived([
// '',
// dayOfWeek('monday', { locale: $locale, style: 'short' }),
// '',
// dayOfWeek('wednesday', { locale: $locale, style: 'short' }),
// '',
// dayOfWeek('friday', { locale: $locale, style: 'short' }),
// '',
// ]);
const weekdays = $derived([
Info.weekdays('short', { locale: $locale })[0],
Info.weekdays('short', { locale: $locale })[2],
Info.weekdays('short', { locale: $locale })[4],
Info.weekdays('short', { locale: $locale })[6],
]);
</script>
<div class="mt-4 w-full">
<div class="relative w-full">
<!-- TODO -->
<!-- <div class="absolute top-4 left-0 flex flex-col gap-0.5">
{#each dayLabels as dayLabel, i (i)}
<div class="relative flex h-3 w-6 items-center text-xs text-gray-500 dark:text-gray-400">
{dayLabel}
</div>
{/each}
</div> -->
<!-- <div class="mb-1 flex justify-between text-xs text-gray-500 dark:text-gray-400">
{#each getUploadActivityMonths() as month (month)}
<div>{month}</div>
{/each}
</div> -->
<div class="grid grid-rows-7 gap-0.5">
{#each rows as row, dayIndex (dayIndex)}
<div class="grid grid-cols-52 gap-0.5">
{#each row as day (day.date)}
<div
class="aspect-square w-full min-w-0 rounded-sm {itemColors(day.count)}"
title={itemLabel({ date: day.date, count: day.count })}
aria-label={itemLabel({ date: day.date, count: day.count })}
></div>
{/each}
</div>
<div class="grid grid-flow-col grid-rows-7 gap-0.5">
<div class="row-span-7 grid grid-rows-subgrid">
{#if Info.getStartOfWeek({ locale: $locale }) === 7}
<div></div>
{/if}
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[0]}</Text></div>
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[1]}</Text></div>
<div class="row-span-2 -mt-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[2]}</Text></div>
{#if Info.getStartOfWeek({ locale: $locale }) === 1}
<div class="-my-1"><Text size="tiny" class="mr-0.5 font-mono">{weekdays[3]}</Text></div>
{/if}
</div>
{#each data.series as day, idx (day.date)}
{@const date = DateTime.fromISO(day.date, { zone: 'utc' }).toLocaleString(
{ month: 'short', day: 'numeric' },
{ locale: $locale },
)}
<div
class="aspect-square size-full rounded-sm {itemColors(day.count)} row-start-(--heatmap-row-start)"
style:--heatmap-row-start={idx === 0 ? padding + 1 : undefined}
title={itemLabel({ date, count: day.count })}
aria-label={itemLabel({ date, count: day.count })}
></div>
{/each}
</div>
@@ -2,7 +2,19 @@
import SchemaAlbumPicker from '$lib/components/SchemaAlbumPicker.svelte';
import Self from '$lib/components/SchemaConfiguration.svelte';
import type { JSONSchemaProperty, SchemaConfig } from '$lib/types';
import { CodeBlock, Field, Input, Label, MultiSelect, NumberInput, Select, Switch, Text } from '@immich/ui';
import {
CodeBlock,
Field,
HelperText,
Input,
Label,
MultiSelect,
NumberInput,
Select,
Switch,
Text,
} from '@immich/ui';
import { t } from 'svelte-i18n';
type Props = {
schema: JSONSchemaProperty;
@@ -75,12 +87,11 @@
<Select options={schema.enum} bind:value={getString, setValue} />
</Field>
{:else if schema.array}
<!-- {@const values = getValue<string[]>([])}
{@const values = getValue<string[]>([])}
<Field {label} {description}>
{#each values as value, i (i)}
<Input bind:value={() => getValue<string>(), setValue} />
{/each}
</Field> -->
<Input value={values.join(',')} disabled />
<HelperText>{$t('unsupported_field_type')}</HelperText>
</Field>
{:else if schema.type === 'boolean'}
<Field {label} {description}>
<Switch bind:checked={() => getBoolean(schema.default ?? false), setValue} />
@@ -628,11 +628,11 @@
{#if stack && withStacked && !assetViewerManager.isShowEditor}
{@const stackedAssets = stack.assets}
<div id="stack-slideshow" class="pointer-events-none absolute bottom-0 col-span-4 col-start-1 w-full">
<div id="stack-slideshow" class="absolute bottom-0 col-span-4 col-start-1 w-fit max-w-full">
<div class="no-wrap horizontal-scrollbar relative flex flex-row overflow-x-auto overflow-y-hidden">
{#each stackedAssets as stackedAsset (stackedAsset.id)}
<div
class={['pointer-events-auto relative inline-block px-1 pb-2 transition-all']}
class={['relative inline-block px-1 pb-2 transition-all']}
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
>
<Thumbnail
@@ -261,6 +261,7 @@
if (!enableRouting) {
invisible = false;
}
scrollableElement?.focus({ preventScroll: true });
});
const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, timelineMonthScrollPercent: number) => {
@@ -2,6 +2,7 @@
import SchemaConfiguration from '$lib/components/SchemaConfiguration.svelte';
import PluginMethodPicker from '$lib/modals/PluginMethodPicker.svelte';
import { type JSONSchemaProperty, type SchemaConfig } from '$lib/types';
import { getWorkflowDefaultConfig } from '$lib/utils/workflow';
import { WorkflowTrigger, type PluginMethodResponseDto, type WorkflowStepDto } from '@immich/sdk';
import { FormModal, IconButton, modalManager, Stack, Text, Textarea } from '@immich/ui';
import { mdiPencilOutline } from '@mdi/js';
@@ -31,7 +32,7 @@
}
method = selected;
config = selected.schema ? {} : null;
config = selected.schema ? getWorkflowDefaultConfig(selected.schema as JSONSchemaProperty) : null;
};
void onPickMethod();
@@ -3,6 +3,7 @@
import { pluginManager } from '$lib/managers/plugin-manager.svelte';
import PluginMethodPicker from '$lib/modals/PluginMethodPicker.svelte';
import { type JSONSchemaProperty, type SchemaConfig } from '$lib/types';
import { getWorkflowDefaultConfig } from '$lib/utils/workflow';
import { WorkflowTrigger, type WorkflowStepDto } from '@immich/sdk';
import { Button, FormModal, modalManager, Stack, Text, Textarea } from '@immich/ui';
import { mdiPencilOutline } from '@mdi/js';
@@ -30,7 +31,7 @@
}
method = selected;
config = selected.schema ? {} : null;
config = selected.schema ? getWorkflowDefaultConfig(selected.schema as JSONSchemaProperty) : null;
};
</script>
+112
View File
@@ -0,0 +1,112 @@
import { getWorkflowDefaultConfig } from '$lib/utils/workflow';
describe(getWorkflowDefaultConfig.name, () => {
describe('required properties', () => {
it('should use a default value', () => {
expect(
getWorkflowDefaultConfig({
type: 'object',
properties: {
test: {
type: 'boolean',
default: true,
},
},
required: ['test'],
}),
).toEqual({ test: true });
});
it('should default to an empty array', () => {
expect(
getWorkflowDefaultConfig({
type: 'object',
properties: {
test: {
type: 'string',
array: true,
},
},
required: ['test'],
}),
).toEqual({ test: [] });
});
it('should default to false', () => {
expect(
getWorkflowDefaultConfig({
type: 'object',
properties: {
test: {
type: 'boolean',
},
},
required: ['test'],
}),
).toEqual({ test: false });
});
it('should default to 0 (integer)', () => {
expect(
getWorkflowDefaultConfig({
type: 'object',
properties: {
test: {
type: 'integer',
},
},
required: ['test'],
}),
).toEqual({ test: 0 });
});
it('should default to 0 (number)', () => {
expect(
getWorkflowDefaultConfig({
type: 'object',
properties: {
test: {
type: 'number',
},
},
required: ['test'],
}),
).toEqual({ test: 0 });
});
it('should default to an empty string', () => {
expect(
getWorkflowDefaultConfig({
type: 'object',
properties: {
test: {
type: 'string',
},
},
required: ['test'],
}),
).toEqual({ test: '' });
});
it('should default recursively', () => {
expect(
getWorkflowDefaultConfig({
type: 'object',
properties: {
parent: {
type: 'object',
properties: {
test: {
type: 'string',
array: true,
},
},
required: ['test'],
},
},
required: ['parent'],
}),
).toEqual({ parent: { test: [] } });
});
});
});
+60
View File
@@ -1,5 +1,6 @@
import { WorkflowTrigger } from '@immich/sdk';
import type { MessageFormatter } from 'svelte-i18n';
import type { JSONSchemaProperty } from '$lib/types';
export const getTriggerName = ($t: MessageFormatter, type: WorkflowTrigger) => {
switch (type) {
@@ -9,6 +10,9 @@ export const getTriggerName = ($t: MessageFormatter, type: WorkflowTrigger) => {
// case WorkflowTrigger.PersonRecognized: {
// return $t('trigger_person_recognized');
// }
case WorkflowTrigger.AssetMetadataExtraction: {
return $t('trigger_asset_metadata_extraction');
}
default: {
return type;
}
@@ -23,8 +27,64 @@ export const getTriggerDescription = ($t: MessageFormatter, type: WorkflowTrigge
// case WorkflowTrigger.PersonRecognized: {
// return $t('trigger_person_recognized_description');
// }
case WorkflowTrigger.AssetMetadataExtraction: {
return $t('trigger_asset_metadata_extraction_description');
}
default: {
return type;
}
}
};
export const getWorkflowDefaultConfig = (schema: JSONSchemaProperty) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const config: any = {};
const requiredProperties = schema.required ?? [];
for (const [key, property] of Object.entries(schema.properties ?? {})) {
// default values
if (property.default) {
config[key] = property.default;
break;
}
if (!requiredProperties.includes(key)) {
continue;
}
if (property.array) {
config[key] = [];
continue;
}
switch (property.type) {
case 'string': {
config[key] = '';
break;
}
case 'integer':
case 'number': {
config[key] = 0;
break;
}
case 'boolean': {
config[key] = false;
break;
}
case 'object': {
config[key] = property.properties ? getWorkflowDefaultConfig(property) : {};
break;
}
default: {
console.log(`Unknown configuration type: ${property.type}`);
}
}
}
return config;
};
@@ -63,9 +63,9 @@
let { data }: Props = $props();
let { id, enabled, name, description, trigger } = $derived(data.workflow);
let { id, enabled, name, description, trigger } = $state(data.workflow);
let steps = $state(data.workflow.steps.map((step) => ({ ...step, id: generateId() })));
let savedWorkflow = $state(cloneDeep(data.workflow));
let savedWorkflow = $derived(cloneDeep(data.workflow));
let allowNavigation = $state(false);
let isShowingNavigationDialog = $state(false);
let isSaving = $state(false);
@@ -238,7 +238,9 @@
inputType={SettingInputFieldType.NUMBER}
{disabled}
label={$t('admin.transcoding_threads')}
description={$t('admin.transcoding_threads_description')}
description={$t('admin.transcoding_threads_description', {
values: { av1: configToEdit.ffmpeg.targetVideoCodec === VideoCodec.Av1 },
})}
bind:value={configToEdit.ffmpeg.threads}
isEdited={configToEdit.ffmpeg.threads !== config.ffmpeg.threads}
/>