From ef72f1ec5b9648ea908a36e12d57f1368b710d0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:32:47 -0400 Subject: [PATCH 01/42] chore(deps): update dependency pillow to v12.3.0 [security] (#30084) --- machine-learning/uv.lock | 168 +++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 85 deletions(-) diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index fa92147445..95bcb54da1 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -3,13 +3,13 @@ revision = 3 requires-python = ">=3.11, <4.0" resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.13' and python_full_version < '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and python_full_version < '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version >= '3.13' and python_full_version < '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", "python_full_version < '3.12' and sys_platform == 'darwin'", "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", @@ -1609,89 +1609,87 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" +version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] From 093f5c070ad14ccc63ff3087e48d5d50f6fbda24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:33:23 -0400 Subject: [PATCH 02/42] chore(deps): update dependency pydantic-settings to v2.14.2 [security] (#29252) --- machine-learning/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 95bcb54da1..d9a542683b 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -1884,16 +1884,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, ] [[package]] From c04c81a979ae5b0802bce4eda65d11564f2b6350 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 26 Aug 2026 12:11:12 -0700 Subject: [PATCH 03/42] chore(mobile): remove Drift* prefixes from classes and variables (#31038) * chore(mobile): remove Drift* prefixes from classes and variables * Remove *Drift* from related mocks --- mobile/lib/domain/models/memory.model.dart | 8 +- .../services/background_worker.service.dart | 4 +- .../lib/domain/services/memory.service.dart | 13 +- .../lib/domain/services/people.service.dart | 5 +- .../domain/services/remote_album.service.dart | 4 +- .../services/sync_linked_album.service.dart | 6 +- .../repositories/memory.repository.dart | 10 +- ...rift_backup.page.dart => backup.page.dart} | 44 +-- ....dart => backup_album_selection.page.dart} | 26 +- ...age.dart => backup_asset_detail.page.dart} | 10 +- ...ons.page.dart => backup_options.page.dart} | 16 +- ...tail.page.dart => upload_detail.page.dart} | 30 +- mobile/lib/pages/common/settings.page.dart | 4 +- .../lib/pages/common/splash_screen.page.dart | 10 +- mobile/lib/pages/common/tab_shell.page.dart | 4 +- .../pages/library/locked/pin_auth.page.dart | 4 +- .../actions/edit_asset.action.dart | 2 +- .../actions/similar_photos.action.dart | 2 +- .../actions/slideshow.action.dart | 3 +- ...ivities.page.dart => activities.page.dart} | 8 +- ...{drift_album.page.dart => album.page.dart} | 10 +- ...ions.page.dart => album_options.page.dart} | 12 +- ...ft_archive.page.dart => archive.page.dart} | 4 +- ...art => asset_selection_timeline.page.dart} | 4 +- ...page.dart => asset_troubleshoot.page.dart} | 0 ...album.page.dart => create_album.page.dart} | 10 +- .../pages/dev/main_timeline.page.dart | 4 +- .../{drift_edit.page.dart => edit.page.dart} | 8 +- ..._favorite.page.dart => favorite.page.dart} | 4 +- ...ft_library.page.dart => library.page.dart} | 24 +- ..._album.page.dart => local_album.page.dart} | 4 +- ...lder.page.dart => locked_folder.page.dart} | 8 +- .../{drift_map.page.dart => map.page.dart} | 8 +- ...rift_memory.page.dart => memory.page.dart} | 14 +- ...y_list.page.dart => memory_list.page.dart} | 16 +- ...ail.page.dart => partner_detail.page.dart} | 4 +- ....page.dart => people_collection.page.dart} | 10 +- ...rift_person.page.dart => person.page.dart} | 8 +- ...{drift_place.page.dart => place.page.dart} | 8 +- ...etail.page.dart => place_detail.page.dart} | 4 +- .../profile/profile_picture_crop.page.dart | 4 +- ...ded.page.dart => recently_added.page.dart} | 4 +- ...ken.page.dart => recently_taken.page.dart} | 4 +- ...album.page.dart => remote_album.page.dart} | 14 +- ...rift_search.page.dart => search.page.dart} | 12 +- ...lideshow.page.dart => slideshow.page.dart} | 8 +- ...{drift_trash.page.dart => trash.page.dart} | 4 +- ...ion.page.dart => user_selection.page.dart} | 8 +- ...{drift_video.page.dart => video.page.dart} | 4 +- ...xt_field.dart => activity_text_field.dart} | 8 +- .../asset_details/people_details.widget.dart | 10 +- .../viewer_top_app_bar.widget.dart | 6 +- .../backup/backup_toggle_button.widget.dart | 10 +- .../presentation/widgets/map/map.widget.dart | 8 +- .../widgets/map/map_settings_sheet.dart | 8 +- .../memory/memory_bottom_info.widget.dart | 6 +- .../widgets/memory/memory_card.widget.dart | 4 +- .../widgets/memory/memory_lane.widget.dart | 24 +- .../person_edit_birthday_modal.widget.dart | 10 +- .../people/person_edit_name_modal.widget.dart | 10 +- ...n.widget.dart => album_option.widget.dart} | 4 +- .../providers/app_life_cycle.provider.dart | 8 +- .../providers/background_sync.provider.dart | 14 +- .../lib/providers/backup/backup.provider.dart | 298 ++++++++++++++++- .../backup/backup_server.provider.dart | 23 ++ .../backup/drift_backup.provider.dart | 300 ------------------ .../infrastructure/album.provider.dart | 4 +- .../infrastructure/memory.provider.dart | 8 +- .../infrastructure/people.provider.dart | 10 +- mobile/lib/providers/routes.provider.dart | 2 +- ...ository.dart => album_api_repository.dart} | 8 +- mobile/lib/routing/router.dart | 130 ++++---- mobile/lib/services/deep_link.service.dart | 16 +- mobile/lib/utils/people.utils.dart | 4 +- mobile/lib/utils/provider_utils.dart | 4 +- .../album/remote_album_shared_user_icons.dart | 2 +- ...st_tile.dart => album_info_list_tile.dart} | 4 +- .../common/app_bar_dialog/app_bar_dialog.dart | 6 +- .../app_bar_dialog/app_bar_profile_info.dart | 4 +- .../widgets/common/immich_sliver_app_bar.dart | 8 +- .../common/remote_album_sliver_app_bar.dart | 2 +- ...kup_settings.dart => backup_settings.dart} | 4 +- .../sync_status_and_actions.dart | 2 +- .../sync_linked_album_service_test.dart | 4 +- .../test/infrastructure/repository.mock.dart | 4 +- ...est.dart => album_option_widget_test.dart} | 215 +++---------- .../app_life_cycle_provider_test.dart | 6 +- ...er_test.dart => backup_provider_test.dart} | 6 +- .../infrastructure/memory_provider_test.dart | 8 +- ...st.dart => album_api_repository_test.dart} | 6 +- .../test/services/deep_link_service_test.dart | 20 +- mobile/test/unit/mocks.dart | 2 +- 92 files changed, 775 insertions(+), 904 deletions(-) rename mobile/lib/pages/backup/{drift_backup.page.dart => backup.page.dart} (92%) rename mobile/lib/pages/backup/{drift_backup_album_selection.page.dart => backup_album_selection.page.dart} (94%) rename mobile/lib/pages/backup/{drift_backup_asset_detail.page.dart => backup_asset_detail.page.dart} (93%) rename mobile/lib/pages/backup/{drift_backup_options.page.dart => backup_options.page.dart} (81%) rename mobile/lib/pages/backup/{drift_upload_detail.page.dart => upload_detail.page.dart} (95%) rename mobile/lib/presentation/pages/{drift_activities.page.dart => activities.page.dart} (90%) rename mobile/lib/presentation/pages/{drift_album.page.dart => album.page.dart} (87%) rename mobile/lib/presentation/pages/{drift_album_options.page.dart => album_options.page.dart} (95%) rename mobile/lib/presentation/pages/{drift_archive.page.dart => archive.page.dart} (93%) rename mobile/lib/presentation/pages/{drift_asset_selection_timeline.page.dart => asset_selection_timeline.page.dart} (88%) rename mobile/lib/presentation/pages/{drift_asset_troubleshoot.page.dart => asset_troubleshoot.page.dart} (100%) rename mobile/lib/presentation/pages/{drift_create_album.page.dart => create_album.page.dart} (97%) rename mobile/lib/presentation/pages/edit/{drift_edit.page.dart => edit.page.dart} (97%) rename mobile/lib/presentation/pages/{drift_favorite.page.dart => favorite.page.dart} (93%) rename mobile/lib/presentation/pages/{drift_library.page.dart => library.page.dart} (95%) rename mobile/lib/presentation/pages/{drift_local_album.page.dart => local_album.page.dart} (96%) rename mobile/lib/presentation/pages/{drift_locked_folder.page.dart => locked_folder.page.dart} (88%) rename mobile/lib/presentation/pages/{drift_map.page.dart => map.page.dart} (89%) rename mobile/lib/presentation/pages/{drift_memory.page.dart => memory.page.dart} (97%) rename mobile/lib/presentation/pages/{drift_memory_list.page.dart => memory_list.page.dart} (85%) rename mobile/lib/presentation/pages/{drift_partner_detail.page.dart => partner_detail.page.dart} (97%) rename mobile/lib/presentation/pages/{drift_people_collection.page.dart => people_collection.page.dart} (93%) rename mobile/lib/presentation/pages/{drift_person.page.dart => person.page.dart} (91%) rename mobile/lib/presentation/pages/{drift_place.page.dart => place.page.dart} (94%) rename mobile/lib/presentation/pages/{drift_place_detail.page.dart => place_detail.page.dart} (88%) rename mobile/lib/presentation/pages/{drift_recently_added.page.dart => recently_added.page.dart} (91%) rename mobile/lib/presentation/pages/{drift_recently_taken.page.dart => recently_taken.page.dart} (91%) rename mobile/lib/presentation/pages/{drift_remote_album.page.dart => remote_album.page.dart} (96%) rename mobile/lib/presentation/pages/search/{drift_search.page.dart => search.page.dart} (98%) rename mobile/lib/presentation/pages/{drift_slideshow.page.dart => slideshow.page.dart} (98%) rename mobile/lib/presentation/pages/{drift_trash.page.dart => trash.page.dart} (98%) rename mobile/lib/presentation/pages/{drift_user_selection.page.dart => user_selection.page.dart} (96%) rename mobile/lib/presentation/pages/{drift_video.page.dart => video.page.dart} (92%) rename mobile/lib/presentation/widgets/album/{drift_activity_text_field.dart => activity_text_field.dart} (92%) rename mobile/lib/presentation/widgets/remote_album/{drift_album_option.widget.dart => album_option.widget.dart} (97%) create mode 100644 mobile/lib/providers/backup/backup_server.provider.dart delete mode 100644 mobile/lib/providers/backup/drift_backup.provider.dart rename mobile/lib/repositories/{drift_album_api_repository.dart => album_api_repository.dart} (95%) rename mobile/lib/widgets/backup/{drift_album_info_list_tile.dart => album_info_list_tile.dart} (96%) rename mobile/lib/widgets/settings/backup_settings/{drift_backup_settings.dart => backup_settings.dart} (99%) rename mobile/test/presentation/widgets/remote_album/{drift_album_option_widget_test.dart => album_option_widget_test.dart} (72%) rename mobile/test/providers/backup/{drift_backup_provider_test.dart => backup_provider_test.dart} (94%) rename mobile/test/repositories/{drift_album_api_repository_test.dart => album_api_repository_test.dart} (94%) diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart index 1c556f382f..35fc98ad63 100644 --- a/mobile/lib/domain/models/memory.model.dart +++ b/mobile/lib/domain/models/memory.model.dart @@ -30,11 +30,11 @@ abstract class MemoryData with _$MemoryData { factory MemoryData.fromJson(String source) => MemoryData.fromMap(json.decode(source) as Map); } -// Model for a memory stored in the server +/// A specialized collection of assets with some novel display mechanism // TODO(agg23): DriftMemoryRepository currently mutates `assets` @Freezed(makeCollectionsUnmodifiable: false) -abstract class DriftMemory with _$DriftMemory { - const factory DriftMemory({ +abstract class Memory with _$Memory { + const factory Memory({ required String id, required DateTime createdAt, required DateTime updatedAt, @@ -48,5 +48,5 @@ abstract class DriftMemory with _$DriftMemory { DateTime? showAt, DateTime? hideAt, required List assets, - }) = _DriftMemory; + }) = _Memory; } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 88a213816a..c4664c5685 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -18,7 +18,7 @@ import 'package:immich_mobile/infrastructure/repositories/settings.repository.da import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; @@ -308,7 +308,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } if (Platform.isIOS) { - return _ref?.read(driftBackupProvider.notifier).startBackupWithURLSession(currentUser.id); + return _ref?.read(backupProvider.notifier).startBackupWithURLSession(currentUser.id); } return _ref diff --git a/mobile/lib/domain/services/memory.service.dart b/mobile/lib/domain/services/memory.service.dart index 8cc474a1ad..311c76d8b3 100644 --- a/mobile/lib/domain/services/memory.service.dart +++ b/mobile/lib/domain/services/memory.service.dart @@ -2,22 +2,23 @@ import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart'; import 'package:logging/logging.dart'; -class DriftMemoryService { - final log = Logger("DriftMemoryService"); +/// Accesses Memories; a specialized collection of assets with some novel display mechanism +class MemoryService { + final log = Logger("MemoryService"); final MemoryRepository _repository; - DriftMemoryService(this._repository); + MemoryService(this._repository); - Future> getMemoryLane(String ownerId) { + Future> getMemoryLane(String ownerId) { return _repository.getAll(ownerId); } - Future> getAll(String ownerId, {bool onlyFavorites = false}) { + Future> getAll(String ownerId, {bool onlyFavorites = false}) { return _repository.getAll(ownerId, onlyToday: false, onlyFavorites: onlyFavorites); } - Future get(String memoryId) { + Future get(String memoryId) { return _repository.get(memoryId); } diff --git a/mobile/lib/domain/services/people.service.dart b/mobile/lib/domain/services/people.service.dart index 21d893cf01..338523f76b 100644 --- a/mobile/lib/domain/services/people.service.dart +++ b/mobile/lib/domain/services/people.service.dart @@ -4,11 +4,12 @@ import 'package:immich_mobile/data/db/main/dao/person.dart'; import 'package:immich_mobile/data/server/person.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -class DriftPeopleService { +/// Accesses People; entities mapped to assets for presence and face detection +class PeopleService { final PeopleRepository _repository; final PersonApiRepository _personApiRepository; - const DriftPeopleService(this._repository, this._personApiRepository); + const PeopleService(this._repository, this._personApiRepository); Future get(String personId) { return _repository.get(personId); diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 8e6a6c8aae..9088620e50 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/repositories/album_api_repository.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:logging/logging.dart'; @@ -26,7 +26,7 @@ class RemoteAlbumService { static final _logger = Logger('RemoteAlbumService'); final RemoteAlbumRepository _repository; - final DriftAlbumApiRepository _albumApiRepository; + final AlbumApiRepository _albumApiRepository; final ForegroundUploadService _uploadService; const RemoteAlbumService(this._repository, this._albumApiRepository, this._uploadService); diff --git a/mobile/lib/domain/services/sync_linked_album.service.dart b/mobile/lib/domain/services/sync_linked_album.service.dart index 80a1fbaf9e..4f57d6df06 100644 --- a/mobile/lib/domain/services/sync_linked_album.service.dart +++ b/mobile/lib/domain/services/sync_linked_album.service.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.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/repositories/album_api_repository.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; @@ -17,7 +17,7 @@ final syncLinkedAlbumServiceProvider = Provider((ref) { return SyncLinkedAlbumService( db.localAlbumRepository, db.remoteAlbumRepository, - ref.watch(driftAlbumApiRepositoryProvider), + ref.watch(albumApiRepositoryProvider), ref.watch(storeServiceProvider), ); }); @@ -25,7 +25,7 @@ final syncLinkedAlbumServiceProvider = Provider((ref) { class SyncLinkedAlbumService { final LocalAlbumRepository _localAlbumRepository; final RemoteAlbumRepository _remoteAlbumRepository; - final DriftAlbumApiRepository _albumApiRepository; + final AlbumApiRepository _albumApiRepository; final StoreService _storeService; SyncLinkedAlbumService( diff --git a/mobile/lib/infrastructure/repositories/memory.repository.dart b/mobile/lib/infrastructure/repositories/memory.repository.dart index 51ed141d2f..09f753db3f 100644 --- a/mobile/lib/infrastructure/repositories/memory.repository.dart +++ b/mobile/lib/infrastructure/repositories/memory.repository.dart @@ -12,7 +12,7 @@ class MemoryRepository extends DatabaseAccessor with $MemoryRepositoryMix Drift get _db => attachedDatabase; - Future> getAll(String ownerId, {bool onlyToday = true, bool onlyFavorites = false}) async { + Future> getAll(String ownerId, {bool onlyToday = true, bool onlyFavorites = false}) async { final query = _db.select(_db.memoryEntity).join([ innerJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)), @@ -45,7 +45,7 @@ class MemoryRepository extends DatabaseAccessor with $MemoryRepositoryMix return const []; } - final Map memoriesMap = {}; + final Map memoriesMap = {}; for (final row in rows) { final memory = row.readTable(_db.memoryEntity); @@ -63,7 +63,7 @@ class MemoryRepository extends DatabaseAccessor with $MemoryRepositoryMix return memoriesMap.values.toList(growable: false); } - Future get(String memoryId) async { + Future get(String memoryId) async { final query = _db.select(_db.memoryEntity).join([ leftOuterJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)), @@ -101,8 +101,8 @@ class MemoryRepository extends DatabaseAccessor with $MemoryRepositoryMix } extension on MemoryEntityData { - DriftMemory toDto() { - return DriftMemory( + Memory toDto() { + return Memory( id: id, createdAt: createdAt, updatedAt: updatedAt, diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/backup.page.dart similarity index 92% rename from mobile/lib/pages/backup/drift_backup.page.dart rename to mobile/lib/pages/backup/backup.page.dart index a0e55e58de..afc98e8b23 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/backup.page.dart @@ -12,8 +12,8 @@ import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/permission.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; @@ -27,14 +27,14 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() -class DriftBackupPage extends ConsumerStatefulWidget { - const DriftBackupPage({super.key}); +class BackupPage extends ConsumerStatefulWidget { + const BackupPage({super.key}); @override - ConsumerState createState() => _DriftBackupPageState(); + ConsumerState createState() => _BackupPageState(); } -class _DriftBackupPageState extends ConsumerState { +class _BackupPageState extends ConsumerState { bool? syncSuccess; @override @@ -49,7 +49,7 @@ class _DriftBackupPageState extends ConsumerState { } WidgetsBinding.instance.addPostFrameCallback((_) async { - final backupNotifier = ref.read(driftBackupProvider.notifier); + final backupNotifier = ref.read(backupProvider.notifier); final syncManager = ref.read(backgroundSyncProvider); await backupNotifier.getBackupStatus(currentUser.id); @@ -77,9 +77,9 @@ class _DriftBackupPageState extends ConsumerState { .where((album) => album.backupSelection == BackupSelection.selected) .toList(); - final error = ref.watch(driftBackupProvider.select((p) => p.error)); + final error = ref.watch(backupProvider.select((p) => p.error)); - final backupNotifier = ref.watch(driftBackupProvider.notifier); + final backupNotifier = ref.watch(backupProvider.notifier); final backupSyncManager = ref.watch(backgroundSyncProvider); Future startBackup() async { @@ -97,7 +97,7 @@ class _DriftBackupPageState extends ConsumerState { await backupNotifier.getBackupStatus(currentUser.id); if (syncSuccess == false) { - Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup"); + Logger("BackupPage").warning("Remote sync did not complete successfully, skipping backup"); return; } await backupNotifier.startForegroundBackup(currentUser.id); @@ -117,7 +117,7 @@ class _DriftBackupPageState extends ConsumerState { actions: [ IconButton( onPressed: () { - unawaited(context.pushRoute(const DriftBackupOptionsRoute())); + unawaited(context.pushRoute(const BackupOptionsRoute())); }, icon: const Icon(Icons.settings_outlined), tooltip: context.t.backup_options, @@ -303,7 +303,7 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin ], TextButton.icon( icon: const Icon(Icons.info_outline_rounded), - onPressed: () => context.pushRoute(const DriftUploadDetailRoute()), + onPressed: () => context.pushRoute(const UploadDetailRoute()), label: Text(context.t.view_details), ), ], @@ -396,7 +396,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { ), trailing: ElevatedButton( onPressed: () async { - await context.pushRoute(const DriftBackupAlbumSelectionRoute()); + await context.pushRoute(const BackupAlbumSelectionRoute()); if (!context.mounted) { return; } @@ -405,7 +405,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { if (currentUser == null) { return; } - unawaited(ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id)); + unawaited(ref.read(backupProvider.notifier).getBackupStatus(currentUser.id)); }, child: Text(context.t.select, style: const TextStyle(fontWeight: FontWeight.bold)), ), @@ -419,7 +419,7 @@ class _TotalCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final totalCount = ref.watch(driftBackupProvider.select((p) => p.totalCount)); + final totalCount = ref.watch(backupProvider.select((p) => p.totalCount)); return BackupInfoCard( title: context.t.total, @@ -434,7 +434,7 @@ class _BackupCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final backupCount = ref.watch(driftBackupProvider.select((p) => p.backupCount)); + final backupCount = ref.watch(backupProvider.select((p) => p.backupCount)); final syncStatus = ref.watch(syncStatusProvider); return BackupInfoCard( @@ -451,7 +451,7 @@ class _RemainderCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount)); + final remainderCount = ref.watch(backupProvider.select((p) => p.remainderCount)); final syncStatus = ref.watch(syncStatusProvider); return Card( @@ -522,7 +522,7 @@ class _RemainderCard extends ConsumerWidget { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), ), - onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()), + onTap: () => context.pushRoute(const BackupAssetDetailRoute()), title: Text( context.t.view_details, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), @@ -559,7 +559,7 @@ class _PreparingStatusState extends ConsumerState { _pollingTimer = Timer.periodic(const Duration(seconds: 3), (timer) async { final currentUser = ref.read(currentUserProvider); if (currentUser != null && mounted) { - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + await ref.read(backupProvider.notifier).getBackupStatus(currentUser.id); if (!context.mounted) { timer.cancel(); _pollingTimer = null; @@ -567,7 +567,7 @@ class _PreparingStatusState extends ConsumerState { } // Stop polling if processing count reaches 0 - final updatedProcessingCount = ref.read(driftBackupProvider.select((p) => p.processingCount)); + final updatedProcessingCount = ref.read(backupProvider.select((p) => p.processingCount)); if (updatedProcessingCount == 0) { timer.cancel(); _pollingTimer = null; @@ -582,11 +582,11 @@ class _PreparingStatusState extends ConsumerState { @override Widget build(BuildContext context) { final syncStatus = ref.watch(syncStatusProvider); - final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount)); - final processingCount = ref.watch(driftBackupProvider.select((p) => p.processingCount)); + final remainderCount = ref.watch(backupProvider.select((p) => p.remainderCount)); + final processingCount = ref.watch(backupProvider.select((p) => p.processingCount)); final readyForUploadCount = remainderCount - processingCount; - ref.listen(driftBackupProvider.select((p) => p.processingCount), (previous, next) { + ref.listen(backupProvider.select((p) => p.processingCount), (previous, next) { if (next > 0 && _pollingTimer == null) { _startPollingIfNeeded(); } else if (next == 0 && _pollingTimer != null) { diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart similarity index 94% rename from mobile/lib/pages/backup/drift_backup_album_selection.page.dart rename to mobile/lib/pages/backup/backup_album_selection.page.dart index 26719c2884..7cbc684d5d 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -9,12 +9,12 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; +import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:logging/logging.dart'; @@ -24,14 +24,14 @@ final backupAlbumCountProvider = FutureProvider.autoDispose((ref) async { }); @RoutePage() -class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget { - const DriftBackupAlbumSelectionPage({super.key}); +class BackupAlbumSelectionPage extends ConsumerStatefulWidget { + const BackupAlbumSelectionPage({super.key}); @override - ConsumerState createState() => _DriftBackupAlbumSelectionPageState(); + ConsumerState createState() => _BackupAlbumSelectionPageState(); } -class _DriftBackupAlbumSelectionPageState extends ConsumerState { +class _BackupAlbumSelectionPageState extends ConsumerState { String _searchQuery = ''; bool _isSearchMode = false; int _initialTotalAssetCount = 0; @@ -49,7 +49,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); + _initialTotalAssetCount = ref.read(backupProvider.select((p) => p.totalCount)); } Future _handlePagePopped() async { @@ -111,14 +111,14 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); + final currentTotalAssetCount = ref.read(backupProvider.select((p) => p.totalCount)); final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; - final backupNotifier = ref.read(driftBackupProvider.notifier); + final backupNotifier = ref.read(backupProvider.notifier); final backgroundSync = ref.read(backgroundSyncProvider); final nativeSync = ref.read(nativeSyncApiProvider); if (totalChanged) { @@ -131,7 +131,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState> result = ref.watch(driftBackupCandidateProvider); + final AsyncValue> result = ref.watch(backupCandidateProvider); return Scaffold( appBar: AppBar(title: Text(context.t.backup_controller_page_remainder)), body: result.when( @@ -30,7 +30,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { itemCount: candidates.length, itemBuilder: (context, index) { final asset = candidates[index]; - final albumsAsyncValue = ref.watch(driftCandidateBackupAlbumInfoProvider(asset.id)); + final albumsAsyncValue = ref.watch(candidateBackupAlbumInfoProvider(asset.id)); final assetMediaRepository = ref.watch(assetMediaRepositoryProvider); return FutureBuilder( future: assetMediaRepository.getOriginalFilename(asset.id), diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/backup_options.page.dart similarity index 81% rename from mobile/lib/pages/backup/drift_backup_options.page.dart rename to mobile/lib/pages/backup/backup_options.page.dart index 994f09766c..db54bf3ed4 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/backup_options.page.dart @@ -6,15 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.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/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; +import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:logging/logging.dart'; @RoutePage() -class DriftBackupOptionsPage extends ConsumerWidget { - const DriftBackupOptionsPage({super.key}); +class BackupOptionsPage extends ConsumerWidget { + const BackupOptionsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -44,7 +44,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { return; } - await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); + await ref.read(backupProvider.notifier).getBackupStatus(currentUser.id); final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled; if (!isBackupEnabled || !context.mounted) { return; @@ -54,7 +54,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { SnackBar(content: Text(context.t.network_requirements_updated), duration: const Duration(seconds: 4)), ); - final backupNotifier = ref.read(driftBackupProvider.notifier); + final backupNotifier = ref.read(backupProvider.notifier); final backgroundSync = ref.read(backgroundSyncProvider); backupNotifier.stopForegroundBackup(reason: "backup settings updated"); unawaited( @@ -62,7 +62,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { if (success) { return backupNotifier.startForegroundBackup(currentUser.id); } else { - Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup'); + Logger('BackupOptionsPage').warning('Background sync failed, not starting backup'); } }), ); @@ -70,7 +70,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { }, child: Scaffold( appBar: AppBar(title: Text(context.t.backup_options)), - body: const DriftBackupSettings(), + body: const BackupSettings(), ), ); } diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/upload_detail.page.dart similarity index 95% rename from mobile/lib/pages/backup/drift_upload_detail.page.dart rename to mobile/lib/pages/backup/upload_detail.page.dart index 764cb5dcf1..809be8aed4 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/upload_detail.page.dart @@ -5,20 +5,20 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:path/path.dart' as path; @RoutePage() -class DriftUploadDetailPage extends ConsumerStatefulWidget { - const DriftUploadDetailPage({super.key}); +class UploadDetailPage extends ConsumerStatefulWidget { + const UploadDetailPage({super.key}); @override - ConsumerState createState() => _DriftUploadDetailPageState(); + ConsumerState createState() => _UploadDetailPageState(); } -class _DriftUploadDetailPageState extends ConsumerState { +class _UploadDetailPageState extends ConsumerState { final Set _seenTaskIds = {}; final Set _failedTaskIds = {}; @@ -26,8 +26,8 @@ class _DriftUploadDetailPageState extends ConsumerState { static const int _maxSlots = 3; /// Assigns uploading items to fixed slots to prevent jumping when items complete - List _assignItemsToSlots(List uploadingItems) { - final slots = List.filled(_maxSlots, null); + List _assignItemsToSlots(List uploadingItems) { + final slots = List.filled(_maxSlots, null); final currentTaskIds = uploadingItems.map((e) => e.taskId).toSet(); _taskSlotAssignments.removeWhere((taskId, _) => !currentTaskIds.contains(taskId)); @@ -58,8 +58,8 @@ class _DriftUploadDetailPageState extends ConsumerState { @override Widget build(BuildContext context) { - final uploadItems = ref.watch(driftBackupProvider.select((state) => state.uploadItems)); - final iCloudProgress = ref.watch(driftBackupProvider.select((state) => state.iCloudDownloadProgress)); + final uploadItems = ref.watch(backupProvider.select((state) => state.uploadItems)); + final iCloudProgress = ref.watch(backupProvider.select((state) => state.iCloudDownloadProgress)); for (final item in uploadItems.values) { if (item.isFailed == true) { @@ -91,8 +91,8 @@ class _DriftUploadDetailPageState extends ConsumerState { Widget _buildTwoSectionLayout( BuildContext context, - List uploadingItems, - List failedItems, + List uploadingItems, + List failedItems, Map iCloudProgress, ) { return CustomScrollView( @@ -276,7 +276,7 @@ class _DriftUploadDetailPageState extends ConsumerState { ); } - Widget _buildCurrentUploadCard(BuildContext context, DriftUploadStatus item) { + Widget _buildCurrentUploadCard(BuildContext context, UploadStatus item) { final double progressPercentage = (item.progress * 100).clamp(0, 100); final isFailed = item.isFailed == true; @@ -365,7 +365,7 @@ class _DriftUploadDetailPageState extends ConsumerState { ); } - Widget _buildErrorCard(BuildContext context, DriftUploadStatus item) { + Widget _buildErrorCard(BuildContext context, UploadStatus item) { return Card( elevation: 0, color: context.colorScheme.errorContainer, @@ -493,7 +493,7 @@ class _DriftUploadDetailPageState extends ConsumerState { ); } - Future _showFileDetailDialog(BuildContext context, DriftUploadStatus item) { + Future _showFileDetailDialog(BuildContext context, UploadStatus item) { return showDialog( context: context, builder: (context) => FileDetailDialog(uploadStatus: item), @@ -538,7 +538,7 @@ class _CurrentUploadThumbnail extends ConsumerWidget { } class FileDetailDialog extends ConsumerWidget { - final DriftUploadStatus uploadStatus; + final UploadStatus uploadStatus; const FileDetailDialog({super.key, required this.uploadStatus}); @override diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 2e941a70a3..bd13d900eb 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; +import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/free_up_space_settings.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart'; @@ -59,7 +59,7 @@ enum SettingSection { Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), - SettingSection.backup => const DriftBackupSettings(), + SettingSection.backup => const BackupSettings(), SettingSection.freeUpSpace => const FreeUpSpaceSettings(), SettingSection.languages => const LanguageSettings(), SettingSection.networking => const NetworkingSettings(), diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index ad13e10cd5..c9a4f58aa3 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -15,7 +15,7 @@ import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.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/backup/backup.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'; @@ -310,7 +310,7 @@ class SplashScreenPageState extends ConsumerState { final infoProvider = ref.read(serverInfoProvider.notifier); final wsProvider = ref.read(websocketProvider.notifier); final backgroundManager = ref.read(backgroundSyncProvider); - final backupProvider = ref.read(driftBackupProvider.notifier); + final backupNotifier = ref.read(backupProvider.notifier); final viewIntentHandler = ref.read(viewIntentHandlerProvider); unawaited( @@ -334,9 +334,9 @@ class SplashScreenPageState extends ConsumerState { if (syncSuccess) { await Future.wait([ backgroundManager.hashAssets().then((_) { - unawaited(_resumeBackup(backupProvider)); + unawaited(_resumeBackup(backupNotifier)); }), - _resumeBackup(backupProvider), + _resumeBackup(backupNotifier), // TODO: Bring back when the soft freeze issue is addressed // backgroundManager.syncCloudIds(), ]); @@ -376,7 +376,7 @@ class SplashScreenPageState extends ConsumerState { } } - Future _resumeBackup(DriftBackupNotifier notifier) async { + Future _resumeBackup(BackupNotifier notifier) async { final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled; if (isEnableBackup) { diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index 1623479adb..23e28e928c 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -77,7 +77,7 @@ class _TabShellPageState extends ConsumerState { } return AutoTabsRouter( - routes: const [MainTimelineRoute(), DriftSearchRoute(), DriftAlbumsRoute(), DriftLibraryRoute()], + routes: const [MainTimelineRoute(), SearchRoute(), AlbumsRoute(), LibraryRoute()], duration: const Duration(milliseconds: 600), transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child), builder: (context, child) { @@ -111,7 +111,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { } if (index == kPhotoTabIndex) { - ref.invalidate(driftMemoryLaneProvider); + ref.invalidate(memoryLaneProvider); } if (router.activeIndex != kSearchTabIndex && index == kSearchTabIndex) { diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index db0e009d8c..9fcb71ea21 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -37,7 +37,7 @@ class PinAuthPage extends HookConsumerWidget { ), ); - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); + unawaited(context.replaceRoute(const LockedFolderRoute())); } Future enableBiometricAuth() { @@ -85,7 +85,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); + unawaited(context.replaceRoute(const LockedFolderRoute())); }, ), ), diff --git a/mobile/lib/presentation/actions/edit_asset.action.dart b/mobile/lib/presentation/actions/edit_asset.action.dart index e2b4993c3b..08cf055176 100644 --- a/mobile/lib/presentation/actions/edit_asset.action.dart +++ b/mobile/lib/presentation/actions/edit_asset.action.dart @@ -58,7 +58,7 @@ class EditAssetAction extends AssetActionBuilder { ref.read(editorStateProvider.notifier).init(edits, exif); unawaited( context.pushRoute( - DriftEditImageRoute( + EditImageRoute( image: Image(image: getFullImageProvider(asset, edited: false)), applyEdits: (newEdits) => applyEdits(ref, asset.id, newEdits), ), diff --git a/mobile/lib/presentation/actions/similar_photos.action.dart b/mobile/lib/presentation/actions/similar_photos.action.dart index 53bb29f09a..d61f24dfa9 100644 --- a/mobile/lib/presentation/actions/similar_photos.action.dart +++ b/mobile/lib/presentation/actions/similar_photos.action.dart @@ -37,6 +37,6 @@ class SimilarPhotosAction extends ActionBuilder { ), ); - unawaited(context.navigateTo(const DriftSearchRoute())); + unawaited(context.navigateTo(const SearchRoute())); } } diff --git a/mobile/lib/presentation/actions/slideshow.action.dart b/mobile/lib/presentation/actions/slideshow.action.dart index 4b6c90b517..c3c3402657 100644 --- a/mobile/lib/presentation/actions/slideshow.action.dart +++ b/mobile/lib/presentation/actions/slideshow.action.dart @@ -15,7 +15,6 @@ class SlideshowAction extends ActionBuilder { ActionItem create(BuildContext context, WidgetRef ref) => .new( icon: Icons.slideshow, label: context.t.slideshow, - onAction: () async => - unawaited(context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider)))), + onAction: () async => unawaited(context.pushRoute(SlideshowRoute(timeline: ref.read(timelineServiceProvider)))), ); } diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/activities.page.dart similarity index 90% rename from mobile/lib/presentation/pages/drift_activities.page.dart rename to mobile/lib/presentation/pages/activities.page.dart index 2bb3507e6e..70d65675ce 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/activities.page.dart @@ -8,18 +8,18 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/album/drift_activity_text_field.dart'; +import 'package:immich_mobile/presentation/widgets/album/activity_text_field.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/widgets/activities/comment_bubble.dart'; @RoutePage() -class DriftActivitiesPage extends HookConsumerWidget { +class ActivitiesPage extends HookConsumerWidget { final RemoteAlbum album; final String? assetId; final String? assetName; - const DriftActivitiesPage({super.key, required this.album, this.assetId, this.assetName}); + const ActivitiesPage({super.key, required this.album, this.assetId, this.assetName}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -82,7 +82,7 @@ class DriftActivitiesPage extends HookConsumerWidget { color: context.scaffoldBackgroundColor, border: Border(top: BorderSide(color: context.colorScheme.secondaryContainer, width: 1)), ), - child: DriftActivityTextField(isEnabled: album.isActivityEnabled, onSubmit: onAddComment), + child: ActivityTextField(isEnabled: album.isActivityEnabled, onSubmit: onAddComment), ), ), ], diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/album.page.dart similarity index 87% rename from mobile/lib/presentation/pages/drift_album.page.dart rename to mobile/lib/presentation/pages/album.page.dart index ab91b6f3c8..92a30b5293 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/album.page.dart @@ -10,14 +10,14 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; @RoutePage() -class DriftAlbumsPage extends ConsumerStatefulWidget { - const DriftAlbumsPage({super.key}); +class AlbumsPage extends ConsumerStatefulWidget { + const AlbumsPage({super.key}); @override - ConsumerState createState() => _DriftAlbumsPageState(); + ConsumerState createState() => _AlbumsPageState(); } -class _DriftAlbumsPageState extends ConsumerState { +class _AlbumsPageState extends ConsumerState { final ScrollController _scrollController = ScrollController(); Future onRefresh() async { @@ -45,7 +45,7 @@ class _DriftAlbumsPageState extends ConsumerState { pinned: true, actions: [ IconButton( - onPressed: () => context.pushRoute(const DriftCreateAlbumRoute()), + onPressed: () => context.pushRoute(const CreateAlbumRoute()), icon: const Icon(Icons.add_rounded), ), ], diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/album_options.page.dart similarity index 95% rename from mobile/lib/presentation/pages/drift_album_options.page.dart rename to mobile/lib/presentation/pages/album_options.page.dart index 078b9cc325..6beb76929c 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/album_options.page.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; +import 'package:immich_mobile/presentation/pages/user_selection.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; @@ -22,9 +22,9 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @RoutePage() -class DriftAlbumOptionsPage extends HookConsumerWidget { +class AlbumOptionsPage extends HookConsumerWidget { final RemoteAlbum album; - const DriftAlbumOptionsPage({super.key, required this.album}); + const AlbumOptionsPage({super.key, required this.album}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -33,7 +33,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { final activityEnabled = useState(album.isActivityEnabled); final isOwner = album.ownerId == userId; final owner = isOwner ? ref.watch(currentUserProvider) : null; - final allUsers = isOwner ? null : ref.watch(driftUsersProvider); + final allUsers = isOwner ? null : ref.watch(usersProvider); void showErrorMessage() { ContextHelper(context).pop(); @@ -52,7 +52,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { return; } - unawaited(context.navigateTo(const DriftAlbumsRoute())); + unawaited(context.navigateTo(const AlbumsRoute())); } catch (_) { showErrorMessage(); } @@ -70,7 +70,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { } Future addUsers() async { - final newUsers = await context.pushRoute>(DriftUserSelectionRoute(album: album)); + final newUsers = await context.pushRoute>(UserSelectionRoute(album: album)); if (newUsers == null || newUsers.isEmpty) { return; diff --git a/mobile/lib/presentation/pages/drift_archive.page.dart b/mobile/lib/presentation/pages/archive.page.dart similarity index 93% rename from mobile/lib/presentation/pages/drift_archive.page.dart rename to mobile/lib/presentation/pages/archive.page.dart index 75c05eb070..82595f98fb 100644 --- a/mobile/lib/presentation/pages/drift_archive.page.dart +++ b/mobile/lib/presentation/pages/archive.page.dart @@ -9,8 +9,8 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftArchivePage extends StatelessWidget { - const DriftArchivePage({super.key}); +class ArchivePage extends StatelessWidget { + const ArchivePage({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart b/mobile/lib/presentation/pages/asset_selection_timeline.page.dart similarity index 88% rename from mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart rename to mobile/lib/presentation/pages/asset_selection_timeline.page.dart index d256cc2220..2a3f7128eb 100644 --- a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart +++ b/mobile/lib/presentation/pages/asset_selection_timeline.page.dart @@ -7,9 +7,9 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @RoutePage() -class DriftAssetSelectionTimelinePage extends StatelessWidget { +class AssetSelectionTimelinePage extends StatelessWidget { final Set lockedSelectionAssets; - const DriftAssetSelectionTimelinePage({super.key, this.lockedSelectionAssets = const {}}); + const AssetSelectionTimelinePage({super.key, this.lockedSelectionAssets = const {}}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/asset_troubleshoot.page.dart similarity index 100% rename from mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart rename to mobile/lib/presentation/pages/asset_troubleshoot.page.dart diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/create_album.page.dart similarity index 97% rename from mobile/lib/presentation/pages/drift_create_album.page.dart rename to mobile/lib/presentation/pages/create_album.page.dart index f536b5b60f..a1c4e11d1c 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/create_album.page.dart @@ -13,14 +13,14 @@ import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @RoutePage() -class DriftCreateAlbumPage extends ConsumerStatefulWidget { - const DriftCreateAlbumPage({super.key}); +class CreateAlbumPage extends ConsumerStatefulWidget { + const CreateAlbumPage({super.key}); @override - ConsumerState createState() => _DriftCreateAlbumPageState(); + ConsumerState createState() => _CreateAlbumPageState(); } -class _DriftCreateAlbumPageState extends ConsumerState { +class _CreateAlbumPageState extends ConsumerState { TextEditingController albumTitleController = TextEditingController(); TextEditingController albumDescriptionController = TextEditingController(); FocusNode albumTitleTextFieldFocusNode = FocusNode(); @@ -153,7 +153,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { Future onSelectPhotos() async { final assets = await context.pushRoute>( - DriftAssetSelectionTimelineRoute(lockedSelectionAssets: selectedAssets), + AssetSelectionTimelineRoute(lockedSelectionAssets: selectedAssets), ); if (assets == null || assets.isEmpty) { diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index 97925c108f..8b1a6c6549 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -39,9 +39,9 @@ class _MainTimelinePageState extends ConsumerState { @override Widget build(BuildContext context) { - final hasMemories = ref.watch(driftMemoryLaneProvider.select((state) => state.value?.isNotEmpty ?? false)); + final hasMemories = ref.watch(memoryLaneProvider.select((state) => state.value?.isNotEmpty ?? false)); return Timeline( - topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()), + topSliverWidget: const SliverToBoxAdapter(child: MemoryLane()), topSliverWidgetHeight: hasMemories ? 200 : 0, showStorageIndicator: true, ); diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/edit.page.dart similarity index 97% rename from mobile/lib/presentation/pages/edit/drift_edit.page.dart rename to mobile/lib/presentation/pages/edit/edit.page.dart index 6237169431..12d7cddf8f 100644 --- a/mobile/lib/presentation/pages/edit/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/edit/edit.page.dart @@ -19,17 +19,17 @@ import 'package:immich_ui/immich_ui.dart'; import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; @RoutePage() -class DriftEditImagePage extends ConsumerStatefulWidget { +class EditImagePage extends ConsumerStatefulWidget { final Image image; final Future Function(List edits) applyEdits; - const DriftEditImagePage({super.key, required this.image, required this.applyEdits}); + const EditImagePage({super.key, required this.image, required this.applyEdits}); @override - ConsumerState createState() => _DriftEditImagePageState(); + ConsumerState createState() => _EditImagePageState(); } -class _DriftEditImagePageState extends ConsumerState with TickerProviderStateMixin { +class _EditImagePageState extends ConsumerState with TickerProviderStateMixin { Future _saveEditedImage() async { ref.read(editorStateProvider.notifier).setIsEditing(true); diff --git a/mobile/lib/presentation/pages/drift_favorite.page.dart b/mobile/lib/presentation/pages/favorite.page.dart similarity index 93% rename from mobile/lib/presentation/pages/drift_favorite.page.dart rename to mobile/lib/presentation/pages/favorite.page.dart index dd4384906b..2898b4f026 100644 --- a/mobile/lib/presentation/pages/drift_favorite.page.dart +++ b/mobile/lib/presentation/pages/favorite.page.dart @@ -9,8 +9,8 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftFavoritePage extends StatelessWidget { - const DriftFavoritePage({super.key}); +class FavoritePage extends StatelessWidget { + const FavoritePage({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/library.page.dart similarity index 95% rename from mobile/lib/presentation/pages/drift_library.page.dart rename to mobile/lib/presentation/pages/library.page.dart index 8a49468233..1c032cba52 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/library.page.dart @@ -22,8 +22,8 @@ import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() -class DriftLibraryPage extends StatelessWidget { - const DriftLibraryPage({super.key}); +class LibraryPage extends StatelessWidget { + const LibraryPage({super.key}); @override Widget build(BuildContext context) { @@ -56,13 +56,13 @@ class _ActionButtonGrid extends ConsumerWidget { children: [ _ActionButton( icon: Icons.favorite_outline_rounded, - onTap: () => context.pushRoute(const DriftFavoriteRoute()), + onTap: () => context.pushRoute(const FavoriteRoute()), label: context.t.favorites, ), const SizedBox(width: 8), _ActionButton( icon: Icons.archive_outlined, - onTap: () => context.pushRoute(const DriftArchiveRoute()), + onTap: () => context.pushRoute(const ArchiveRoute()), label: context.t.archived, ), ], @@ -79,7 +79,7 @@ class _ActionButtonGrid extends ConsumerWidget { isTrashEnable ? _ActionButton( icon: Icons.delete_outline_rounded, - onTap: () => context.pushRoute(const DriftTrashRoute()), + onTap: () => context.pushRoute(const TrashRoute()), label: context.t.trash, ) : const SizedBox.shrink(), @@ -161,7 +161,7 @@ class _PeopleCollectionCard extends ConsumerWidget { final size = context.width * widthFactor - 20.0; return GestureDetector( - onTap: () => context.pushRoute(const DriftPeopleCollectionRoute()), + onTap: () => context.pushRoute(const PeopleCollectionRoute()), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -226,7 +226,7 @@ class _PlacesCollectionCard extends StatelessWidget { final size = context.width * widthFactor - 20.0; return GestureDetector( - onTap: () => context.pushRoute(DriftPlaceRoute(currentLocation: null)), + onTap: () => context.pushRoute(PlaceRoute(currentLocation: null)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -280,7 +280,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget { final size = context.width * widthFactor - 20.0; return GestureDetector( - onTap: () => context.pushRoute(const DriftLocalAlbumsRoute()), + onTap: () => context.pushRoute(const LocalAlbumsRoute()), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -341,7 +341,7 @@ class _MemoriesCollectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final memories = ref.watch(driftAllMemoriesProvider(false)); + final memories = ref.watch(allMemoriesProvider(false)); return LayoutBuilder( builder: (context, constraints) { @@ -350,7 +350,7 @@ class _MemoriesCollectionCard extends ConsumerWidget { final size = context.width * widthFactor - 20.0; return GestureDetector( - onTap: () => context.pushRoute(const DriftMemoryListRoute()), + onTap: () => context.pushRoute(const MemoryListRoute()), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -469,7 +469,7 @@ class _QuickAccessButtonList extends ConsumerWidget { context.t.locked_folder, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), - onTap: () => context.pushRoute(const DriftLockedFolderRoute()), + onTap: () => context.pushRoute(const LockedFolderRoute()), ), ListTile( leading: const Icon(Icons.group_outlined, size: 26), @@ -516,7 +516,7 @@ class _PartnerList extends StatelessWidget { context.t.partner_list_user_photos(user: partner.name), style: const TextStyle(fontWeight: FontWeight.w500), ), - onTap: () => context.pushRoute(DriftPartnerDetailRoute(partner: partner)), + onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)), ); }, ); diff --git a/mobile/lib/presentation/pages/drift_local_album.page.dart b/mobile/lib/presentation/pages/local_album.page.dart similarity index 96% rename from mobile/lib/presentation/pages/drift_local_album.page.dart rename to mobile/lib/presentation/pages/local_album.page.dart index 71707688da..e3337eeb94 100644 --- a/mobile/lib/presentation/pages/drift_local_album.page.dart +++ b/mobile/lib/presentation/pages/local_album.page.dart @@ -11,8 +11,8 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/local_album_sliver_app_bar.dart'; @RoutePage() -class DriftLocalAlbumsPage extends StatelessWidget { - const DriftLocalAlbumsPage({super.key}); +class LocalAlbumsPage extends StatelessWidget { + const LocalAlbumsPage({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/locked_folder.page.dart similarity index 88% rename from mobile/lib/presentation/pages/drift_locked_folder.page.dart rename to mobile/lib/presentation/pages/locked_folder.page.dart index 6ebe7283d3..b6a9aad075 100644 --- a/mobile/lib/presentation/pages/drift_locked_folder.page.dart +++ b/mobile/lib/presentation/pages/locked_folder.page.dart @@ -13,14 +13,14 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftLockedFolderPage extends ConsumerStatefulWidget { - const DriftLockedFolderPage({super.key}); +class LockedFolderPage extends ConsumerStatefulWidget { + const LockedFolderPage({super.key}); @override - ConsumerState createState() => _DriftLockedFolderPageState(); + ConsumerState createState() => _LockedFolderPageState(); } -class _DriftLockedFolderPageState extends ConsumerState with WidgetsBindingObserver { +class _LockedFolderPageState extends ConsumerState with WidgetsBindingObserver { bool _showOverlay = false; @override diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/map.page.dart similarity index 89% rename from mobile/lib/presentation/pages/drift_map.page.dart rename to mobile/lib/presentation/pages/map.page.dart index d36ccc350c..07518052bf 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/map.page.dart @@ -8,10 +8,10 @@ import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() -class DriftMapPage extends StatelessWidget { +class MapPage extends StatelessWidget { final LatLng? initialLocation; - const DriftMapPage({super.key, this.initialLocation}); + const MapPage({super.key, this.initialLocation}); Future onSettingsPressed(BuildContext context) { return showModalBottomSheet( @@ -19,7 +19,7 @@ class DriftMapPage extends StatelessWidget { showDragHandle: true, isScrollControlled: true, context: context, - builder: (_) => const DriftMapSettingsSheet(), + builder: (_) => const MapSettingsSheet(), ); } @@ -29,7 +29,7 @@ class DriftMapPage extends StatelessWidget { extendBodyBehindAppBar: true, body: Stack( children: [ - DriftMap(initialLocation: initialLocation), + MapView(initialLocation: initialLocation), Positioned( left: 20, top: 70, diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/memory.page.dart similarity index 97% rename from mobile/lib/presentation/pages/drift_memory.page.dart rename to mobile/lib/presentation/pages/memory.page.dart index 3ad7d90657..df59c8ebb8 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/memory.page.dart @@ -20,13 +20,13 @@ import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; /// Expects the current asset to be set via [assetViewerProvider] before navigating to this page @RoutePage() -class DriftMemoryPage extends HookConsumerWidget { - final List memories; +class MemoryPage extends HookConsumerWidget { + final List memories; final int memoryIndex; - const DriftMemoryPage({required this.memories, required this.memoryIndex, super.key}); + const MemoryPage({required this.memories, required this.memoryIndex, super.key}); - static void setMemory(WidgetRef ref, DriftMemory memory) { + static void setMemory(WidgetRef ref, Memory memory) { if (memory.assets.isNotEmpty) { ref.read(assetViewerProvider.notifier).setAsset(memory.assets.first); } @@ -221,7 +221,7 @@ class DriftMemoryPage extends HookConsumerWidget { currentMemory.value = memories[pageNumber]; WidgetsBinding.instance.addPostFrameCallback((_) { - DriftMemoryPage.setMemory(ref, memories[pageNumber]); + MemoryPage.setMemory(ref, memories[pageNumber]); }); // Update currentAsset to the first asset of the new memory @@ -285,7 +285,7 @@ class DriftMemoryPage extends HookConsumerWidget { children: [ ColoredBox( color: Colors.black, - child: DriftMemoryCard( + child: MemoryCard( asset: asset, title: title, showTitle: index == 0, @@ -348,7 +348,7 @@ class DriftMemoryPage extends HookConsumerWidget { ], ), ), - DriftMemoryBottomInfo(memory: memories[mIndex], title: title), + MemoryBottomInfo(memory: memories[mIndex], title: title), ], ); }, diff --git a/mobile/lib/presentation/pages/drift_memory_list.page.dart b/mobile/lib/presentation/pages/memory_list.page.dart similarity index 85% rename from mobile/lib/presentation/pages/drift_memory_list.page.dart rename to mobile/lib/presentation/pages/memory_list.page.dart index ddf9fd8ae9..f2e018b2d6 100644 --- a/mobile/lib/presentation/pages/drift_memory_list.page.dart +++ b/mobile/lib/presentation/pages/memory_list.page.dart @@ -6,25 +6,25 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; +import 'package:immich_mobile/presentation/pages/memory.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @RoutePage() -class DriftMemoryListPage extends ConsumerStatefulWidget { - const DriftMemoryListPage({super.key}); +class MemoryListPage extends ConsumerStatefulWidget { + const MemoryListPage({super.key}); @override - ConsumerState createState() => _DriftMemoryListPageState(); + ConsumerState createState() => _MemoryListPageState(); } -class _DriftMemoryListPageState extends ConsumerState { +class _MemoryListPageState extends ConsumerState { bool _onlyFavorites = false; @override Widget build(BuildContext context) { - final memories = ref.watch(driftAllMemoriesProvider(_onlyFavorites)); + final memories = ref.watch(allMemoriesProvider(_onlyFavorites)); return LayoutBuilder( builder: (context, constraints) { @@ -54,8 +54,8 @@ class _DriftMemoryListPageState extends ConsumerState { itemCount: memories.length, itemBuilder: (context, index) => GestureDetector( onTap: () { - DriftMemoryPage.setMemory(ref, memories[index]); - unawaited(context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index))); + MemoryPage.setMemory(ref, memories[index]); + unawaited(context.pushRoute(MemoryRoute(memories: memories, memoryIndex: index))); }, child: Stack( children: [ diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/partner_detail.page.dart similarity index 97% rename from mobile/lib/presentation/pages/drift_partner_detail.page.dart rename to mobile/lib/presentation/pages/partner_detail.page.dart index 70de434d9f..2e03a8ab5a 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/partner_detail.page.dart @@ -13,10 +13,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftPartnerDetailPage extends StatelessWidget { +class PartnerDetailPage extends StatelessWidget { final Partner partner; - const DriftPartnerDetailPage({super.key, required this.partner}); + const PartnerDetailPage({super.key, required this.partner}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/people_collection.page.dart similarity index 93% rename from mobile/lib/presentation/pages/drift_people_collection.page.dart rename to mobile/lib/presentation/pages/people_collection.page.dart index 92f1ff79ca..c03f5157ba 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/people_collection.page.dart @@ -15,14 +15,14 @@ import 'package:immich_mobile/utils/people.utils.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @RoutePage() -class DriftPeopleCollectionPage extends ConsumerStatefulWidget { - const DriftPeopleCollectionPage({super.key}); +class PeopleCollectionPage extends ConsumerStatefulWidget { + const PeopleCollectionPage({super.key}); @override - ConsumerState createState() => _DriftPeopleCollectionPageState(); + ConsumerState createState() => _PeopleCollectionPageState(); } -class _DriftPeopleCollectionPageState extends ConsumerState { +class _PeopleCollectionPageState extends ConsumerState { final FocusNode _formFocus = FocusNode(); String? _search; @@ -92,7 +92,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState createState() => _DriftPersonPageState(); + ConsumerState createState() => _PersonPageState(); } -class _DriftPersonPageState extends ConsumerState { +class _PersonPageState extends ConsumerState { late Person _person; @override diff --git a/mobile/lib/presentation/pages/drift_place.page.dart b/mobile/lib/presentation/pages/place.page.dart similarity index 94% rename from mobile/lib/presentation/pages/drift_place.page.dart rename to mobile/lib/presentation/pages/place.page.dart index 0d33f2ab29..395f92a406 100644 --- a/mobile/lib/presentation/pages/drift_place.page.dart +++ b/mobile/lib/presentation/pages/place.page.dart @@ -13,8 +13,8 @@ import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() -class DriftPlacePage extends StatelessWidget { - const DriftPlacePage({super.key, this.currentLocation}); +class PlacePage extends StatelessWidget { + const PlacePage({super.key, this.currentLocation}); final LatLng? currentLocation; @@ -94,7 +94,7 @@ class _Map extends StatelessWidget { height: 200, width: context.width, child: MapThumbnail( - onTap: (_, _) => context.pushRoute(DriftMapRoute(initialLocation: currentLocation)), + onTap: (_, _) => context.pushRoute(MapRoute(initialLocation: currentLocation)), zoom: 8, centre: currentLocation ?? const LatLng(21.44950, -157.91959), showAttribution: false, @@ -163,7 +163,7 @@ class _PlaceTile extends StatelessWidget { @override Widget build(BuildContext context) { return LargeLeadingTile( - onTap: () => context.pushRoute(DriftPlaceDetailRoute(place: place.$1)), + onTap: () => context.pushRoute(PlaceDetailRoute(place: place.$1)), title: Text(place.$1, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(20)), diff --git a/mobile/lib/presentation/pages/drift_place_detail.page.dart b/mobile/lib/presentation/pages/place_detail.page.dart similarity index 88% rename from mobile/lib/presentation/pages/drift_place_detail.page.dart rename to mobile/lib/presentation/pages/place_detail.page.dart index 0f50227945..ea0547f802 100644 --- a/mobile/lib/presentation/pages/drift_place_detail.page.dart +++ b/mobile/lib/presentation/pages/place_detail.page.dart @@ -6,10 +6,10 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftPlaceDetailPage extends StatelessWidget { +class PlaceDetailPage extends StatelessWidget { final String place; - const DriftPlaceDetailPage({super.key, required this.place}); + const PlaceDetailPage({super.key, required this.place}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart index a5c069671c..899c41e827 100644 --- a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_server.provider.dart'; import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/utils/image_converter.dart'; @@ -89,7 +89,7 @@ class _ProfilePictureCropPageState extends ConsumerState if (user != null) { unawaited(ref.read(currentUserProvider.notifier).refresh()); } - unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); + unawaited(ref.read(backupServerProvider.notifier).updateDiskInfo()); ImmichToast.show( context: context, diff --git a/mobile/lib/presentation/pages/drift_recently_added.page.dart b/mobile/lib/presentation/pages/recently_added.page.dart similarity index 91% rename from mobile/lib/presentation/pages/drift_recently_added.page.dart rename to mobile/lib/presentation/pages/recently_added.page.dart index 841fb9a671..e5cdb432f0 100644 --- a/mobile/lib/presentation/pages/drift_recently_added.page.dart +++ b/mobile/lib/presentation/pages/recently_added.page.dart @@ -8,8 +8,8 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftRecentlyAddedPage extends StatelessWidget { - const DriftRecentlyAddedPage({super.key}); +class RecentlyAddedPage extends StatelessWidget { + const RecentlyAddedPage({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_recently_taken.page.dart b/mobile/lib/presentation/pages/recently_taken.page.dart similarity index 91% rename from mobile/lib/presentation/pages/drift_recently_taken.page.dart rename to mobile/lib/presentation/pages/recently_taken.page.dart index 22d89770fe..e778d24f68 100644 --- a/mobile/lib/presentation/pages/drift_recently_taken.page.dart +++ b/mobile/lib/presentation/pages/recently_taken.page.dart @@ -8,8 +8,8 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftRecentlyTakenPage extends StatelessWidget { - const DriftRecentlyTakenPage({super.key}); +class RecentlyTakenPage extends StatelessWidget { + const RecentlyTakenPage({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/remote_album.page.dart similarity index 96% rename from mobile/lib/presentation/pages/drift_remote_album.page.dart rename to mobile/lib/presentation/pages/remote_album.page.dart index 7669b6c765..dc51f2e750 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/remote_album.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/album/pending_uploads_banner.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart'; -import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; +import 'package:immich_mobile/presentation/widgets/remote_album/album_option.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; @@ -47,7 +47,7 @@ class _RemoteAlbumPageState extends ConsumerState { } final newAssets = await context.pushRoute>( - DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()), + AssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()), ); if (newAssets == null || newAssets.isEmpty) { @@ -69,7 +69,7 @@ class _RemoteAlbumPageState extends ConsumerState { } Future addUsers(BuildContext context) async { - final newUsers = await context.pushRoute>(DriftUserSelectionRoute(album: _album)); + final newUsers = await context.pushRoute>(UserSelectionRoute(album: _album)); if (newUsers == null || newUsers.isEmpty) { return; @@ -145,7 +145,7 @@ class _RemoteAlbumPageState extends ConsumerState { ImmichToast.show(context: context, msg: context.t.album_deleted, toastType: ToastType.success); - unawaited(context.pushRoute(const DriftAlbumsRoute())); + unawaited(context.pushRoute(const AlbumsRoute())); } catch (e) { if (!context.mounted) { return; @@ -180,7 +180,7 @@ class _RemoteAlbumPageState extends ConsumerState { } Future showActivity(BuildContext context) async { - unawaited(context.pushRoute(DriftActivitiesRoute(album: _album))); + unawaited(context.pushRoute(ActivitiesRoute(album: _album))); } @override @@ -209,7 +209,7 @@ class _RemoteAlbumPageState extends ConsumerState { onToggleAlbumOrder: () => toggleAlbumOrder(), onEditAlbum: () => showEditTitleAndDescription(context), onCreateSharedLink: () => unawaited(context.pushRoute(SharedLinkEditRoute(albumId: _album.id))), - onShowOptions: () => context.pushRoute(DriftAlbumOptionsRoute(album: _album)), + onShowOptions: () => context.pushRoute(AlbumOptionsRoute(album: _album)), ), onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null, onActivity: () => showActivity(context), @@ -428,7 +428,7 @@ class _AlbumKebabMenu extends ConsumerWidget { builder: (context, snapshot) { final canAddPhotos = snapshot.data ?? false; - return DriftRemoteAlbumOption( + return RemoteAlbumOption( iconColor: iconColor, iconShadows: iconShadows, onDeleteAlbum: isOwner ? onDeleteAlbum : null, diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/search.page.dart similarity index 98% rename from mobile/lib/presentation/pages/search/drift_search.page.dart rename to mobile/lib/presentation/pages/search/search.page.dart index 1f2fe506b2..9a4e567f0c 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/search.page.dart @@ -37,8 +37,8 @@ import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.d import 'package:immich_mobile/widgets/search/search_filter/star_rating_picker.dart'; @RoutePage() -class DriftSearchPage extends HookConsumerWidget { - const DriftSearchPage({super.key}); +class SearchPage extends HookConsumerWidget { + const SearchPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -892,23 +892,23 @@ class _QuickLinkList extends StatelessWidget { title: context.t.recently_taken, icon: Icons.schedule_outlined, isTop: true, - onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()), + onTap: () => context.pushRoute(const RecentlyTakenRoute()), ), _QuickLink( title: context.t.recently_added, icon: Icons.upload_outlined, - onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()), + onTap: () => context.pushRoute(const RecentlyAddedRoute()), ), _QuickLink( title: context.t.videos, icon: Icons.play_circle_outline_rounded, - onTap: () => context.pushRoute(const DriftVideoRoute()), + onTap: () => context.pushRoute(const VideoRoute()), ), _QuickLink( title: context.t.favorites, icon: Icons.favorite_border_rounded, isBottom: true, - onTap: () => context.pushRoute(const DriftFavoriteRoute()), + onTap: () => context.pushRoute(const FavoriteRoute()), ), ], ), diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/slideshow.page.dart similarity index 98% rename from mobile/lib/presentation/pages/drift_slideshow.page.dart rename to mobile/lib/presentation/pages/slideshow.page.dart index 2662c60a30..a7df990331 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/slideshow.page.dart @@ -26,16 +26,16 @@ import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() -class DriftSlideshowPage extends ConsumerStatefulWidget { +class SlideshowPage extends ConsumerStatefulWidget { final TimelineService timeline; - const DriftSlideshowPage({super.key, required this.timeline}); + const SlideshowPage({super.key, required this.timeline}); @override - ConsumerState createState() => _DriftSlideshowPageState(); + ConsumerState createState() => _SlideshowPageState(); } -class _DriftSlideshowPageState extends ConsumerState with SingleTickerProviderStateMixin { +class _SlideshowPageState extends ConsumerState with SingleTickerProviderStateMixin { static const double _kenBurnsZoom = 0.1; late SlideshowConfig _config; diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/trash.page.dart similarity index 98% rename from mobile/lib/presentation/pages/drift_trash.page.dart rename to mobile/lib/presentation/pages/trash.page.dart index 4dd027494d..3ab875f639 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/trash.page.dart @@ -14,8 +14,8 @@ import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @RoutePage() -class DriftTrashPage extends StatelessWidget { - const DriftTrashPage({super.key}); +class TrashPage extends StatelessWidget { + const TrashPage({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/user_selection.page.dart similarity index 96% rename from mobile/lib/presentation/pages/drift_user_selection.page.dart rename to mobile/lib/presentation/pages/user_selection.page.dart index c11d5a1fd6..e5837ead83 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/user_selection.page.dart @@ -15,7 +15,7 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; // TODO: Refactor this provider when we have user provider/service/repository pattern in place -final driftUsersProvider = FutureProvider.autoDispose>((ref) async { +final usersProvider = FutureProvider.autoDispose>((ref) async { final drift = ref.watch(driftProvider); final currentUser = ref.watch(currentUserProvider); @@ -44,14 +44,14 @@ final driftUsersProvider = FutureProvider.autoDispose>((ref) async }); @RoutePage() -class DriftUserSelectionPage extends HookConsumerWidget { +class UserSelectionPage extends HookConsumerWidget { final RemoteAlbum album; - const DriftUserSelectionPage({super.key, required this.album}); + const UserSelectionPage({super.key, required this.album}); @override Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = ref.watch(driftUsersProvider); + final AsyncValue> suggestedShareUsers = ref.watch(usersProvider); final sharedUsers = ref.watch(remoteAlbumSharedUsersProvider(album.id)); final sharedUsersList = useState>({}); diff --git a/mobile/lib/presentation/pages/drift_video.page.dart b/mobile/lib/presentation/pages/video.page.dart similarity index 92% rename from mobile/lib/presentation/pages/drift_video.page.dart rename to mobile/lib/presentation/pages/video.page.dart index b51ebc25b8..73adcbf4cd 100644 --- a/mobile/lib/presentation/pages/drift_video.page.dart +++ b/mobile/lib/presentation/pages/video.page.dart @@ -8,8 +8,8 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftVideoPage extends StatelessWidget { - const DriftVideoPage({super.key}); +class VideoPage extends StatelessWidget { + const VideoPage({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart b/mobile/lib/presentation/widgets/album/activity_text_field.dart similarity index 92% rename from mobile/lib/presentation/widgets/album/drift_activity_text_field.dart rename to mobile/lib/presentation/widgets/album/activity_text_field.dart index 55024d9b2b..ed63c1cdf9 100644 --- a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart +++ b/mobile/lib/presentation/widgets/album/activity_text_field.dart @@ -5,14 +5,14 @@ import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -class DriftActivityTextField extends ConsumerStatefulWidget { +class ActivityTextField extends ConsumerStatefulWidget { final bool isEnabled; final bool isBottomSheet; final String? likeId; final Function(String) onSubmit; final Function()? onKeyboardFocus; - const DriftActivityTextField({ + const ActivityTextField({ required this.onSubmit, this.isEnabled = true, this.likeId, @@ -22,10 +22,10 @@ class DriftActivityTextField extends ConsumerStatefulWidget { }); @override - ConsumerState createState() => _DriftActivityTextFieldState(); + ConsumerState createState() => _ActivityTextFieldState(); } -class _DriftActivityTextFieldState extends ConsumerState { +class _ActivityTextFieldState extends ConsumerState { late FocusNode inputFocusNode; late TextEditingController inputController; bool sendEnabled = false; diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart index 00c20f35bd..18838dedec 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart @@ -28,18 +28,18 @@ class PeopleDetails extends ConsumerWidget { return const SizedBox.shrink(); } - final peopleFuture = ref.watch(driftPeopleAssetProvider(asset.id)); + final peopleFuture = ref.watch(peopleAssetProvider(asset.id)); Future showNameEditModal(Person person) async { await showDialog( context: context, useRootNavigator: false, builder: (BuildContext context) { - return DriftPersonNameEditForm(person: person); + return PersonNameEditForm(person: person); }, ); - ref.invalidate(driftPeopleAssetProvider(asset.id)); + ref.invalidate(peopleAssetProvider(asset.id)); } return peopleFuture.when( @@ -71,12 +71,12 @@ class PeopleDetails extends ConsumerWidget { final previousRouteArgs = previousRouteData?.arguments; // Prevent circular navigation - if (previousRouteArgs is DriftPersonRouteArgs && previousRouteArgs.person.id == person.id) { + if (previousRouteArgs is PersonRouteArgs && previousRouteArgs.person.id == person.id) { context.back(); return; } ContextHelper(context).pop(); - unawaited(context.pushRoute(DriftPersonRoute(person: person))); + unawaited(context.pushRoute(PersonRoute(person: person))); }, onNameTap: () => showNameEditModal(person), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 30dc8ecfc3..994f3785e4 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -55,11 +55,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { onPressed: () { unawaited( context.router.push( - DriftActivitiesRoute( - album: album, - assetId: asset is RemoteAsset ? asset.id : null, - assetName: asset.name, - ), + ActivitiesRoute(album: album, assetId: asset is RemoteAsset ? asset.id : null, assetName: asset.name), ), ); }, diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index 383ec17952..9415d83cb6 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; class BackupToggleButton extends ConsumerStatefulWidget { @@ -55,13 +55,13 @@ class BackupToggleButtonState extends ConsumerState with Sin @override Widget build(BuildContext context) { - final uploadTasks = ref.watch(driftBackupProvider.select((state) => state.uploadItems)); + final uploadTasks = ref.watch(backupProvider.select((state) => state.uploadItems)); - final isSyncing = ref.watch(driftBackupProvider.select((state) => state.isSyncing)); + final isSyncing = ref.watch(backupProvider.select((state) => state.isSyncing)); - final iCloudProgress = ref.watch(driftBackupProvider.select((state) => state.iCloudDownloadProgress)); + final iCloudProgress = ref.watch(backupProvider.select((state) => state.iCloudDownloadProgress)); - final errorCount = ref.watch(driftBackupProvider.select((state) => state.errorCount)); + final errorCount = ref.watch(backupProvider.select((state) => state.errorCount)); final isProcessing = uploadTasks.isNotEmpty || isSyncing || iCloudProgress.isNotEmpty; diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index ab2db23d8b..97b9e1a46e 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -39,16 +39,16 @@ class CustomSourceProperties implements SourceProperties { } } -class DriftMap extends ConsumerStatefulWidget { +class MapView extends ConsumerStatefulWidget { final LatLng? initialLocation; - const DriftMap({super.key, this.initialLocation}); + const MapView({super.key, this.initialLocation}); @override - ConsumerState createState() => _DriftMapState(); + ConsumerState createState() => _MapViewState(); } -class _DriftMapState extends ConsumerState { +class _MapViewState extends ConsumerState { MapLibreMapController? mapController; final _reloadMutex = AsyncMutex(); final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2)); diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index c2a5678ee9..43860ec61d 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -8,14 +8,14 @@ import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.da import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; -class DriftMapSettingsSheet extends ConsumerStatefulWidget { - const DriftMapSettingsSheet({super.key}); +class MapSettingsSheet extends ConsumerStatefulWidget { + const MapSettingsSheet({super.key}); @override - ConsumerState createState() => _DriftMapSettingsSheetState(); + ConsumerState createState() => _MapSettingsSheetState(); } -class _DriftMapSettingsSheetState extends ConsumerState { +class _MapSettingsSheetState extends ConsumerState { late bool useCustomRange; @override diff --git a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart index eb8ce1815a..2212961bbe 100644 --- a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart @@ -7,10 +7,10 @@ import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:intl/intl.dart'; -class DriftMemoryBottomInfo extends StatelessWidget { - final DriftMemory memory; +class MemoryBottomInfo extends StatelessWidget { + final Memory memory; final String title; - const DriftMemoryBottomInfo({super.key, required this.memory, required this.title}); + const MemoryBottomInfo({super.key, required this.memory, required this.title}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index 7e782a3db4..e4b9b5b60f 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -9,14 +9,14 @@ import 'package:immich_mobile/presentation/widgets/images/full_image.widget.dart import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -class DriftMemoryCard extends StatelessWidget { +class MemoryCard extends StatelessWidget { final RemoteAsset asset; final String title; final bool showTitle; final bool isCurrent; final Function()? onVideoEnded; - const DriftMemoryCard({ + const MemoryCard({ required this.asset, required this.title, required this.showTitle, diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index c8b75cf6b5..5f9283d467 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -5,19 +5,19 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; +import 'package:immich_mobile/presentation/pages/memory.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -class DriftMemoryLane extends ConsumerWidget { - const DriftMemoryLane({super.key}); +class MemoryLane extends ConsumerWidget { + const MemoryLane({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final memoryLaneProvider = ref.watch(driftMemoryLaneProvider); - final memories = memoryLaneProvider.value ?? const []; + final memoryLane = ref.watch(memoryLaneProvider); + final memories = memoryLane.value ?? const []; if (memories.isEmpty) { return const SizedBox.shrink(); } @@ -33,22 +33,20 @@ class DriftMemoryLane extends ConsumerWidget { onTap: (index) { ref.read(hapticFeedbackProvider.notifier).heavyImpact(); if (memories[index].assets.isNotEmpty) { - DriftMemoryPage.setMemory(ref, memories[index]); + MemoryPage.setMemory(ref, memories[index]); } - unawaited(context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index))); + unawaited(context.pushRoute(MemoryRoute(memories: memories, memoryIndex: index))); }, - children: memories - .map((memory) => DriftMemoryCard(key: Key(memory.id), memory: memory)) - .toList(growable: false), + children: memories.map((memory) => MemoryCard(key: Key(memory.id), memory: memory)).toList(growable: false), ), ); } } -class DriftMemoryCard extends StatelessWidget { - const DriftMemoryCard({super.key, required this.memory}); +class MemoryCard extends StatelessWidget { + const MemoryCard({super.key, required this.memory}); - final DriftMemory memory; + final Memory memory; @override Widget build(BuildContext context) { diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index e56a21e021..e594f1533c 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -10,16 +10,16 @@ import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:scroll_date_picker/scroll_date_picker.dart'; -class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { +class PersonBirthdayEditForm extends ConsumerStatefulWidget { final Person person; - const DriftPersonBirthdayEditForm({super.key, required this.person}); + const PersonBirthdayEditForm({super.key, required this.person}); @override - ConsumerState createState() => _DriftPersonNameEditFormState(); + ConsumerState createState() => _PersonNameEditFormState(); } -class _DriftPersonNameEditFormState extends ConsumerState { +class _PersonNameEditFormState extends ConsumerState { late DateTime _selectedDate; @override @@ -30,7 +30,7 @@ class _DriftPersonNameEditFormState extends ConsumerState saveBirthday() async { try { - final result = await ref.read(driftPeopleServiceProvider).updateBirthday(widget.person.id, _selectedDate); + final result = await ref.read(peopleServiceProvider).updateBirthday(widget.person.id, _selectedDate); if (result != 0 && mounted) { context.pop(_selectedDate); diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index 4ae41bc640..d120f2324a 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -8,16 +8,16 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -class DriftPersonNameEditForm extends ConsumerStatefulWidget { +class PersonNameEditForm extends ConsumerStatefulWidget { final Person person; - const DriftPersonNameEditForm({super.key, required this.person}); + const PersonNameEditForm({super.key, required this.person}); @override - ConsumerState createState() => _DriftPersonNameEditFormState(); + ConsumerState createState() => _PersonNameEditFormState(); } -class _DriftPersonNameEditFormState extends ConsumerState { +class _PersonNameEditFormState extends ConsumerState { late TextEditingController _formController; @override @@ -28,7 +28,7 @@ class _DriftPersonNameEditFormState extends ConsumerState onEdit(String personId, String newName) async { try { - final result = await ref.read(driftPeopleServiceProvider).updateName(personId, newName); + final result = await ref.read(peopleServiceProvider).updateName(personId, newName); if (result != 0 && mounted) { context.pop(newName); } diff --git a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart b/mobile/lib/presentation/widgets/remote_album/album_option.widget.dart similarity index 97% rename from mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart rename to mobile/lib/presentation/widgets/remote_album/album_option.widget.dart index dc89a25788..0dae42e2a2 100644 --- a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart +++ b/mobile/lib/presentation/widgets/remote_album/album_option.widget.dart @@ -3,8 +3,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -class DriftRemoteAlbumOption extends StatelessWidget { - const DriftRemoteAlbumOption({ +class RemoteAlbumOption extends StatelessWidget { + const RemoteAlbumOption({ super.key, this.onAddPhotos, this.onAddUsers, diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 1c7478850d..74a0f5b573 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -7,7 +7,7 @@ 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'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; @@ -128,7 +128,7 @@ class AppLifeCycleNotifier extends StateNotifier { syncSuccess = await backgroundManager.syncRemote(); }, "syncRemote"), ]); - _ref.invalidate(driftAllMemoriesProvider); + _ref.invalidate(allMemoriesProvider); if (syncSuccess) { await Future.wait([ _safeRun(backgroundManager.hashAssets, "hashAssets").then((_) { @@ -157,7 +157,7 @@ class AppLifeCycleNotifier extends StateNotifier { final currentUser = Store.tryGet(StoreKey.currentUser); if (currentUser != null) { await _safeRun( - () => _ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id), + () => _ref.read(backupProvider.notifier).startForegroundBackup(currentUser.id), "handleBackupResume", ); } @@ -207,7 +207,7 @@ class AppLifeCycleNotifier extends StateNotifier { Future _performPause() { if (_ref.read(authProvider).isAuthenticated) { - _ref.read(driftBackupProvider.notifier).stopForegroundBackup(reason: "the app being sent to the background"); + _ref.read(backupProvider.notifier).stopForegroundBackup(reason: "the app being sent to the background"); _ref.read(websocketProvider.notifier).disconnect(); } diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart index 5873e992b1..00477e4d38 100644 --- a/mobile/lib/providers/background_sync.provider.dart +++ b/mobile/lib/providers/background_sync.provider.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; final backgroundSyncProvider = Provider((ref) { @@ -10,17 +10,17 @@ final backgroundSyncProvider = Provider((ref) { onRemoteSyncStart: () { syncStatusNotifier.startRemoteSync(); // ignore: dispose-provided-instances - final backupProvider = ref.read(driftBackupProvider.notifier); - if (backupProvider.mounted) { - backupProvider.updateError(BackupError.none); + final backupNotifier = ref.read(backupProvider.notifier); + if (backupNotifier.mounted) { + backupNotifier.updateError(BackupError.none); } }, onRemoteSyncComplete: (isSuccess) { syncStatusNotifier.completeRemoteSync(); // ignore: dispose-provided-instances - final backupProvider = ref.read(driftBackupProvider.notifier); - if (backupProvider.mounted) { - backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed); + final backupNotifier = ref.read(backupProvider.notifier); + if (backupNotifier.mounted) { + backupNotifier.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed); } }, onRemoteSyncError: syncStatusNotifier.errorRemoteSync, diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index a6dc272313..fe8cc8805a 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,23 +1,297 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; -import 'package:immich_mobile/services/server_info.service.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/background_upload.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/utils/upload_speed_calculator.dart'; +import 'package:logging/logging.dart'; -final backupProvider = StateNotifierProvider((ref) { - return BackupNotifier(ref.watch(serverInfoServiceProvider)); +part 'backup.provider.freezed.dart'; + +@freezed +abstract class EnqueueStatus with _$EnqueueStatus { + const factory EnqueueStatus({required int enqueueCount, required int totalCount}) = _EnqueueStatus; +} + +@freezed +abstract class UploadStatus with _$UploadStatus { + const factory UploadStatus({ + required String taskId, + required String filename, + required double progress, + required int fileSize, + required String networkSpeedAsString, + bool? isFailed, + String? error, + }) = _UploadStatus; +} + +enum BackupError { none, syncFailed } + +@freezed +abstract class BackupState with _$BackupState { + const BackupState._(); + + const factory BackupState({ + required int totalCount, + required int backupCount, + required int remainderCount, + required int processingCount, + required bool isSyncing, + @Default(BackupError.none) BackupError error, + required Map uploadItems, + @Default({}) Map iCloudDownloadProgress, + }) = _BackupState; + + int get errorCount => uploadItems.values.where((item) => item.isFailed == true).length; +} + +final backupProvider = StateNotifierProvider((ref) { + return BackupNotifier( + ref.watch(foregroundUploadServiceProvider), + ref.watch(backgroundUploadServiceProvider), + UploadSpeedManager(), + ); }); -class BackupNotifier extends StateNotifier { - BackupNotifier(this._serverInfoService) - : super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0)); +class BackupNotifier extends StateNotifier { + BackupNotifier(this._foregroundUploadService, this._backgroundUploadService, this._uploadSpeedManager) + : super( + const BackupState( + totalCount: 0, + backupCount: 0, + remainderCount: 0, + processingCount: 0, + isSyncing: false, + uploadItems: {}, + error: BackupError.none, + ), + ); - final ServerInfoService _serverInfoService; + final ForegroundUploadService _foregroundUploadService; + final BackgroundUploadService _backgroundUploadService; + final UploadSpeedManager _uploadSpeedManager; + Completer? _cancelToken; - Future updateDiskInfo() async { - final diskInfo = await _serverInfoService.getDiskInfo(); - if (diskInfo != null) { - state = diskInfo; + final _logger = Logger("BackupNotifier"); + + /// Remove upload item from state + void _removeUploadItem(String taskId) { + if (!mounted) { + _logger.warning("Skip _removeUploadItem: notifier disposed"); + return; + } + if (state.uploadItems.containsKey(taskId)) { + final updatedItems = Map.from(state.uploadItems); + updatedItems.remove(taskId); + state = state.copyWith(uploadItems: updatedItems); } } + + Future getBackupStatus(String userId) async { + if (!mounted) { + _logger.warning("Skip getBackupStatus (pre-call): notifier disposed"); + return; + } + final counts = await _foregroundUploadService.getBackupCounts(userId); + if (!mounted) { + _logger.warning("Skip getBackupStatus (post-call): notifier disposed"); + return; + } + + state = state.copyWith( + totalCount: counts.total, + backupCount: counts.total - counts.remainder, + remainderCount: counts.remainder, + processingCount: counts.processing, + ); + } + + void updateError(BackupError error) { + if (!mounted) { + _logger.warning("Skip updateError: notifier disposed"); + return; + } + state = state.copyWith(error: error); + } + + void updateSyncing(bool isSyncing) { + state = state.copyWith(isSyncing: isSyncing); + } + + Future startForegroundBackup(String userId) async { + // Cancel any existing backup before starting a new one + if (_cancelToken != null) { + stopForegroundBackup(reason: "restarting the backup"); + } + + state = state.copyWith(error: BackupError.none); + + // A pause during the recount below nulls _cancelToken, so the run keeps its own reference. + final cancelToken = Completer(); + _cancelToken = cancelToken; + + // Re-baseline the counters against the same DB read that feeds this run's candidate list, + // otherwise a resume counts duplicate successes against the old baseline (#26215). + await getBackupStatus(userId); + + return _foregroundUploadService.uploadCandidates( + userId, + cancelToken, + callbacks: UploadCallbacks( + onProgress: _handleForegroundBackupProgress, + onSuccess: _handleForegroundBackupSuccess, + onError: _handleForegroundBackupError, + onICloudProgress: _handleICloudProgress, + ), + ); + } + + void stopForegroundBackup({required String reason}) { + if (_cancelToken != null) { + _logger.info("Foreground backup cancelled: $reason"); + } + _cancelToken?.complete(); + _cancelToken = null; + _uploadSpeedManager.clear(); + state = state.copyWith(uploadItems: {}, iCloudDownloadProgress: {}); + } + + void _handleICloudProgress(String localAssetId, double progress) { + state = state.copyWith(iCloudDownloadProgress: {...state.iCloudDownloadProgress, localAssetId: progress}); + + if (progress >= 1.0) { + Future.delayed(const Duration(milliseconds: 250), () { + final updatedProgress = Map.from(state.iCloudDownloadProgress); + updatedProgress.remove(localAssetId); + state = state.copyWith(iCloudDownloadProgress: updatedProgress); + }); + } + } + + void _handleForegroundBackupProgress(String localAssetId, String filename, int bytes, int totalBytes) { + if (_cancelToken == null) { + return; + } + + final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; + final networkSpeedAsString = _uploadSpeedManager.updateProgress(localAssetId, bytes, totalBytes); + final currentItem = state.uploadItems[localAssetId]; + if (currentItem != null) { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: currentItem.copyWith( + filename: filename, + progress: progress, + fileSize: totalBytes, + networkSpeedAsString: networkSpeedAsString, + ), + }, + ); + } else { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: UploadStatus( + taskId: localAssetId, + filename: filename, + progress: progress, + fileSize: totalBytes, + networkSpeedAsString: networkSpeedAsString, + ), + }, + ); + } + } + + void _handleForegroundBackupSuccess(String localAssetId, String remoteAssetId) { + if (!mounted) { + _logger.warning("Skip _handleForegroundBackupSuccess: notifier disposed"); + return; + } + state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1); + _uploadSpeedManager.removeTask(localAssetId); + + Future.delayed(const Duration(milliseconds: 1000), () { + _removeUploadItem(localAssetId); + }); + } + + void _handleForegroundBackupError(String localAssetId, String errorMessage) { + _logger.severe("Upload failed for $localAssetId: $errorMessage"); + + final currentItem = state.uploadItems[localAssetId]; + if (currentItem != null) { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: currentItem.copyWith(isFailed: true, error: errorMessage), + }, + ); + } else { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: UploadStatus( + taskId: localAssetId, + filename: 'Unknown', + progress: 0, + fileSize: 0, + networkSpeedAsString: '', + isFailed: true, + error: errorMessage, + ), + }, + ); + } + + _uploadSpeedManager.removeTask(localAssetId); + } + + Future startBackupWithURLSession(String userId) async { + if (!mounted) { + _logger.warning("Skip handleBackupResume (pre-call): notifier disposed"); + return; + } + _logger.info("Start background backup sequence"); + state = state.copyWith(error: BackupError.none); + final tasks = await _backgroundUploadService.getActiveTasks(kBackupGroup); + if (!mounted) { + _logger.warning("Skip handleBackupResume (post-call): notifier disposed"); + return; + } + _logger.info("Found ${tasks.length} pending tasks"); + + if (tasks.isEmpty) { + _logger.info("No pending tasks, starting new upload"); + return _backgroundUploadService.uploadBackupCandidates(userId); + } + + _logger.info("Resuming upload ${tasks.length} assets"); + return _backgroundUploadService.resume(); + } } + +final backupCandidateProvider = FutureProvider.autoDispose>((ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + return []; + } + + return ref.read(foregroundUploadServiceProvider).getBackupCandidates(user.id, onlyHashed: false); +}); + +final candidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family, String>((ref, assetId) { + return ref + .read(driftProvider) + .localAssetRepository + .getSourceAlbums(assetId, backupSelection: BackupSelection.selected); +}); diff --git a/mobile/lib/providers/backup/backup_server.provider.dart b/mobile/lib/providers/backup/backup_server.provider.dart new file mode 100644 index 0000000000..20b12d837d --- /dev/null +++ b/mobile/lib/providers/backup/backup_server.provider.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; +import 'package:immich_mobile/services/server_info.service.dart'; + +final backupServerProvider = StateNotifierProvider((ref) { + return BackupServerNotifier(ref.watch(serverInfoServiceProvider)); +}); + +class BackupServerNotifier extends StateNotifier { + BackupServerNotifier(this._serverInfoService) + : super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0)); + + final ServerInfoService _serverInfoService; + + Future updateDiskInfo() async { + final diskInfo = await _serverInfoService.getDiskInfo(); + if (diskInfo != null) { + state = diskInfo; + } + } +} diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart deleted file mode 100644 index 57482bdd11..0000000000 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ /dev/null @@ -1,300 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/background_upload.service.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/utils/upload_speed_calculator.dart'; -import 'package:logging/logging.dart'; - -part 'drift_backup.provider.freezed.dart'; - -@freezed -abstract class EnqueueStatus with _$EnqueueStatus { - const factory EnqueueStatus({required int enqueueCount, required int totalCount}) = _EnqueueStatus; -} - -@freezed -abstract class DriftUploadStatus with _$DriftUploadStatus { - const factory DriftUploadStatus({ - required String taskId, - required String filename, - required double progress, - required int fileSize, - required String networkSpeedAsString, - bool? isFailed, - String? error, - }) = _DriftUploadStatus; -} - -enum BackupError { none, syncFailed } - -@freezed -abstract class DriftBackupState with _$DriftBackupState { - const DriftBackupState._(); - - const factory DriftBackupState({ - required int totalCount, - required int backupCount, - required int remainderCount, - required int processingCount, - required bool isSyncing, - @Default(BackupError.none) BackupError error, - required Map uploadItems, - @Default({}) Map iCloudDownloadProgress, - }) = _DriftBackupState; - - int get errorCount => uploadItems.values.where((item) => item.isFailed == true).length; -} - -final driftBackupProvider = StateNotifierProvider((ref) { - return DriftBackupNotifier( - ref.watch(foregroundUploadServiceProvider), - ref.watch(backgroundUploadServiceProvider), - UploadSpeedManager(), - ); -}); - -class DriftBackupNotifier extends StateNotifier { - DriftBackupNotifier(this._foregroundUploadService, this._backgroundUploadService, this._uploadSpeedManager) - : super( - const DriftBackupState( - totalCount: 0, - backupCount: 0, - remainderCount: 0, - processingCount: 0, - isSyncing: false, - uploadItems: {}, - error: BackupError.none, - ), - ); - - final ForegroundUploadService _foregroundUploadService; - final BackgroundUploadService _backgroundUploadService; - final UploadSpeedManager _uploadSpeedManager; - Completer? _cancelToken; - - final _logger = Logger("DriftBackupNotifier"); - - /// Remove upload item from state - void _removeUploadItem(String taskId) { - if (!mounted) { - _logger.warning("Skip _removeUploadItem: notifier disposed"); - return; - } - if (state.uploadItems.containsKey(taskId)) { - final updatedItems = Map.from(state.uploadItems); - updatedItems.remove(taskId); - state = state.copyWith(uploadItems: updatedItems); - } - } - - Future getBackupStatus(String userId) async { - if (!mounted) { - _logger.warning("Skip getBackupStatus (pre-call): notifier disposed"); - return; - } - final counts = await _foregroundUploadService.getBackupCounts(userId); - if (!mounted) { - _logger.warning("Skip getBackupStatus (post-call): notifier disposed"); - return; - } - - state = state.copyWith( - totalCount: counts.total, - backupCount: counts.total - counts.remainder, - remainderCount: counts.remainder, - processingCount: counts.processing, - ); - } - - void updateError(BackupError error) { - if (!mounted) { - _logger.warning("Skip updateError: notifier disposed"); - return; - } - state = state.copyWith(error: error); - } - - void updateSyncing(bool isSyncing) { - state = state.copyWith(isSyncing: isSyncing); - } - - Future startForegroundBackup(String userId) async { - // Cancel any existing backup before starting a new one - if (_cancelToken != null) { - stopForegroundBackup(reason: "restarting the backup"); - } - - state = state.copyWith(error: BackupError.none); - - // A pause during the recount below nulls _cancelToken, so the run keeps its own reference. - final cancelToken = Completer(); - _cancelToken = cancelToken; - - // Re-baseline the counters against the same DB read that feeds this run's candidate list, - // otherwise a resume counts duplicate successes against the old baseline (#26215). - await getBackupStatus(userId); - - return _foregroundUploadService.uploadCandidates( - userId, - cancelToken, - callbacks: UploadCallbacks( - onProgress: _handleForegroundBackupProgress, - onSuccess: _handleForegroundBackupSuccess, - onError: _handleForegroundBackupError, - onICloudProgress: _handleICloudProgress, - ), - ); - } - - void stopForegroundBackup({required String reason}) { - if (_cancelToken != null) { - _logger.info("Foreground backup cancelled: $reason"); - } - _cancelToken?.complete(); - _cancelToken = null; - _uploadSpeedManager.clear(); - state = state.copyWith(uploadItems: {}, iCloudDownloadProgress: {}); - } - - void _handleICloudProgress(String localAssetId, double progress) { - state = state.copyWith(iCloudDownloadProgress: {...state.iCloudDownloadProgress, localAssetId: progress}); - - if (progress >= 1.0) { - Future.delayed(const Duration(milliseconds: 250), () { - final updatedProgress = Map.from(state.iCloudDownloadProgress); - updatedProgress.remove(localAssetId); - state = state.copyWith(iCloudDownloadProgress: updatedProgress); - }); - } - } - - void _handleForegroundBackupProgress(String localAssetId, String filename, int bytes, int totalBytes) { - if (_cancelToken == null) { - return; - } - - final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; - final networkSpeedAsString = _uploadSpeedManager.updateProgress(localAssetId, bytes, totalBytes); - final currentItem = state.uploadItems[localAssetId]; - if (currentItem != null) { - state = state.copyWith( - uploadItems: { - ...state.uploadItems, - localAssetId: currentItem.copyWith( - filename: filename, - progress: progress, - fileSize: totalBytes, - networkSpeedAsString: networkSpeedAsString, - ), - }, - ); - } else { - state = state.copyWith( - uploadItems: { - ...state.uploadItems, - localAssetId: DriftUploadStatus( - taskId: localAssetId, - filename: filename, - progress: progress, - fileSize: totalBytes, - networkSpeedAsString: networkSpeedAsString, - ), - }, - ); - } - } - - void _handleForegroundBackupSuccess(String localAssetId, String remoteAssetId) { - if (!mounted) { - _logger.warning("Skip _handleForegroundBackupSuccess: notifier disposed"); - return; - } - state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1); - _uploadSpeedManager.removeTask(localAssetId); - - Future.delayed(const Duration(milliseconds: 1000), () { - _removeUploadItem(localAssetId); - }); - } - - void _handleForegroundBackupError(String localAssetId, String errorMessage) { - _logger.severe("Upload failed for $localAssetId: $errorMessage"); - - final currentItem = state.uploadItems[localAssetId]; - if (currentItem != null) { - state = state.copyWith( - uploadItems: { - ...state.uploadItems, - localAssetId: currentItem.copyWith(isFailed: true, error: errorMessage), - }, - ); - } else { - state = state.copyWith( - uploadItems: { - ...state.uploadItems, - localAssetId: DriftUploadStatus( - taskId: localAssetId, - filename: 'Unknown', - progress: 0, - fileSize: 0, - networkSpeedAsString: '', - isFailed: true, - error: errorMessage, - ), - }, - ); - } - - _uploadSpeedManager.removeTask(localAssetId); - } - - Future startBackupWithURLSession(String userId) async { - if (!mounted) { - _logger.warning("Skip handleBackupResume (pre-call): notifier disposed"); - return; - } - _logger.info("Start background backup sequence"); - state = state.copyWith(error: BackupError.none); - final tasks = await _backgroundUploadService.getActiveTasks(kBackupGroup); - if (!mounted) { - _logger.warning("Skip handleBackupResume (post-call): notifier disposed"); - return; - } - _logger.info("Found ${tasks.length} pending tasks"); - - if (tasks.isEmpty) { - _logger.info("No pending tasks, starting new upload"); - return _backgroundUploadService.uploadBackupCandidates(userId); - } - - _logger.info("Resuming upload ${tasks.length} assets"); - return _backgroundUploadService.resume(); - } -} - -final driftBackupCandidateProvider = FutureProvider.autoDispose>((ref) { - final user = ref.watch(currentUserProvider); - if (user == null) { - return []; - } - - return ref.read(foregroundUploadServiceProvider).getBackupCandidates(user.id, onlyHashed: false); -}); - -final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family, String>(( - ref, - assetId, -) { - return ref - .read(driftProvider) - .localAssetRepository - .getSourceAlbums(assetId, backupSelection: BackupSelection.selected); -}); diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart index 579cc17838..47fd2360b0 100644 --- a/mobile/lib/providers/infrastructure/album.provider.dart +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/repositories/album_api_repository.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; final localAlbumProvider = FutureProvider>( @@ -23,7 +23,7 @@ final localAlbumThumbnailProvider = FutureProvider.family( final remoteAlbumServiceProvider = Provider( (ref) => RemoteAlbumService( ref.watch(driftProvider).remoteAlbumRepository, - ref.watch(driftAlbumApiRepositoryProvider), + ref.watch(albumApiRepositoryProvider), ref.watch(foregroundUploadServiceProvider), ), ); diff --git a/mobile/lib/providers/infrastructure/memory.provider.dart b/mobile/lib/providers/infrastructure/memory.provider.dart index 1581ea9282..2362fec2c0 100644 --- a/mobile/lib/providers/infrastructure/memory.provider.dart +++ b/mobile/lib/providers/infrastructure/memory.provider.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -final driftMemoryLaneProvider = FutureProvider.autoDispose>((ref) { +final memoryLaneProvider = FutureProvider.autoDispose>((ref) { final (userId, enabled) = ref.watch(currentUserProvider.select((user) => (user?.id, user?.memoryEnabled ?? true))); if (userId == null || !enabled) { return const []; @@ -17,16 +17,16 @@ final driftMemoryLaneProvider = FutureProvider.autoDispose>((r final timer = Timer(nextMidnight.difference(now) + const Duration(seconds: 5), ref.invalidateSelf); ref.onDispose(timer.cancel); - final service = DriftMemoryService(ref.watch(driftProvider).memoryRepository); + final service = MemoryService(ref.watch(driftProvider).memoryRepository); return service.getMemoryLane(userId); }); -final driftAllMemoriesProvider = FutureProvider.autoDispose.family, bool>((ref, onlyFavorites) { +final allMemoriesProvider = FutureProvider.autoDispose.family, bool>((ref, onlyFavorites) { final (userId, enabled) = ref.watch(currentUserProvider.select((user) => (user?.id, user?.memoryEnabled ?? true))); if (userId == null || !enabled) { return const []; } - final service = DriftMemoryService(ref.watch(driftProvider).memoryRepository); + final service = MemoryService(ref.watch(driftProvider).memoryRepository); return service.getAll(userId, onlyFavorites: onlyFavorites); }); diff --git a/mobile/lib/providers/infrastructure/people.provider.dart b/mobile/lib/providers/infrastructure/people.provider.dart index a2f900d6e0..bc27f646f5 100644 --- a/mobile/lib/providers/infrastructure/people.provider.dart +++ b/mobile/lib/providers/infrastructure/people.provider.dart @@ -5,17 +5,17 @@ import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; -final driftPeopleServiceProvider = Provider( - (ref) => DriftPeopleService(ref.watch(driftProvider).peopleRepository, ref.watch(personApiRepositoryProvider)), +final peopleServiceProvider = Provider( + (ref) => PeopleService(ref.watch(driftProvider).peopleRepository, ref.watch(personApiRepositoryProvider)), ); -final driftPeopleAssetProvider = FutureProvider.family, String>((ref, assetId) async { - final service = ref.watch(driftPeopleServiceProvider); +final peopleAssetProvider = FutureProvider.family, String>((ref, assetId) async { + final service = ref.watch(peopleServiceProvider); return service.getAssetPeople(assetId); }); final getAllPeopleProvider = StreamProvider>((ref) async* { - final service = ref.watch(driftPeopleServiceProvider); + final service = ref.watch(peopleServiceProvider); final prefs = await ref.watch(userMetadataPreferencesProvider.future); yield* service.watch(minFaces: prefs?.minimumFaces ?? 3); }); diff --git a/mobile/lib/providers/routes.provider.dart b/mobile/lib/providers/routes.provider.dart index 776389b330..f7dfff70a5 100644 --- a/mobile/lib/providers/routes.provider.dart +++ b/mobile/lib/providers/routes.provider.dart @@ -8,7 +8,7 @@ bool isRouteInStack(Ref ref, String routeName) { return router.stackData.any((route) => route.name == routeName); } -final inLockedViewProvider = Provider((ref) => isRouteInStack(ref, DriftLockedFolderRoute.name)); +final inLockedViewProvider = Provider((ref) => isRouteInStack(ref, LockedFolderRoute.name)); final isAssetViewerOpenProvider = Provider((ref) => isRouteInStack(ref, AssetViewerRoute.name)); final currentRouteNameProvider = StateProvider((ref) => null); diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/album_api_repository.dart similarity index 95% rename from mobile/lib/repositories/drift_album_api_repository.dart rename to mobile/lib/repositories/album_api_repository.dart index 8c21af6599..11d915826e 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/album_api_repository.dart @@ -6,14 +6,12 @@ import 'package:immich_mobile/repositories/api.repository.dart'; // ignore: import_rule_openapi import 'package:openapi/api.dart' hide AlbumUserRole; -final driftAlbumApiRepositoryProvider = Provider( - (ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi), -); +final albumApiRepositoryProvider = Provider((ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi)); -class DriftAlbumApiRepository extends ApiRepository { +class AlbumApiRepository extends ApiRepository { final AlbumsApi _api; - DriftAlbumApiRepository(this._api); + AlbumApiRepository(this._api); Future createDriftAlbum( String name, diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index baa2602492..9499670810 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -14,11 +14,11 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; -import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; -import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; -import 'package:immich_mobile/pages/backup/drift_backup_asset_detail.page.dart'; -import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart'; -import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; +import 'package:immich_mobile/pages/backup/backup.page.dart'; +import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; +import 'package:immich_mobile/pages/backup/backup_asset_detail.page.dart'; +import 'package:immich_mobile/pages/backup/backup_options.page.dart'; +import 'package:immich_mobile/pages/backup/upload_detail.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; @@ -35,41 +35,41 @@ import 'package:immich_mobile/pages/login/login.page.dart'; import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/activities.page.dart'; +import 'package:immich_mobile/presentation/pages/album.page.dart'; +import 'package:immich_mobile/presentation/pages/album_options.page.dart'; +import 'package:immich_mobile/presentation/pages/archive.page.dart'; +import 'package:immich_mobile/presentation/pages/asset_selection_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/asset_troubleshoot.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; +import 'package:immich_mobile/presentation/pages/create_album.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; import 'package:immich_mobile/presentation/pages/download_info.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_asset_troubleshoot.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_map.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_memory_list.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_people_collection.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_person.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_place.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_recently_added.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_slideshow.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; -import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; +import 'package:immich_mobile/presentation/pages/edit/edit.page.dart'; +import 'package:immich_mobile/presentation/pages/favorite.page.dart'; import 'package:immich_mobile/presentation/pages/feature_message/whats_new.page.dart'; +import 'package:immich_mobile/presentation/pages/library.page.dart'; +import 'package:immich_mobile/presentation/pages/local_album.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/locked_folder.page.dart'; +import 'package:immich_mobile/presentation/pages/map.page.dart'; +import 'package:immich_mobile/presentation/pages/memory.page.dart'; +import 'package:immich_mobile/presentation/pages/memory_list.page.dart'; +import 'package:immich_mobile/presentation/pages/partner_detail.page.dart'; +import 'package:immich_mobile/presentation/pages/people_collection.page.dart'; +import 'package:immich_mobile/presentation/pages/person.page.dart'; +import 'package:immich_mobile/presentation/pages/place.page.dart'; +import 'package:immich_mobile/presentation/pages/place_detail.page.dart'; import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; +import 'package:immich_mobile/presentation/pages/recently_added.page.dart'; +import 'package:immich_mobile/presentation/pages/recently_taken.page.dart'; +import 'package:immich_mobile/presentation/pages/remote_album.page.dart'; +import 'package:immich_mobile/presentation/pages/search/search.page.dart'; +import 'package:immich_mobile/presentation/pages/slideshow.page.dart'; +import 'package:immich_mobile/presentation/pages/trash.page.dart'; +import 'package:immich_mobile/presentation/pages/user_selection.page.dart'; +import 'package:immich_mobile/presentation/pages/video.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; @@ -123,9 +123,9 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], children: [ AutoRoute(page: MainTimelineRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftSearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), + AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], ), AutoRoute(page: ProfilePictureCropRoute.page), @@ -143,8 +143,8 @@ class AppRouter extends RootStackRouter { AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftBackupRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftBackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: BackupRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: BackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalTimelineRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: MainTimelineRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteAlbumRoute.page, guards: [_authGuard]), @@ -161,38 +161,38 @@ class AppRouter extends RootStackRouter { ), ), ), - AutoRoute(page: DriftMemoryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftFavoriteRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftTrashRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftArchiveRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftLockedFolderRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), - AutoRoute(page: DriftVideoRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftAssetSelectionTimelineRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftPartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftRecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftRecentlyAddedRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftLocalAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftCreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: MemoryRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: FavoriteRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: LockedFolderRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), + AutoRoute(page: VideoRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: AssetSelectionTimelineRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: RecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: RecentlyAddedRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: LocalAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: PlaceRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: PlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: UserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: UploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), - AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]), - AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftAlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftMapRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftEditImageRoute.page), - AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: PeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: PersonRoute.page, guards: [_authGuard]), + AutoRoute(page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: MapRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: EditImageRoute.page), + AutoRoute(page: ActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: BackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: CleanupPreviewRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftSlideshowRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: DriftMemoryListRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: SlideshowRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: MemoryListRoute.page, guards: [_authGuard, _duplicateGuard]), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 738cb78721..2f593ffba9 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -21,8 +21,8 @@ final deepLinkServiceProvider = Provider( ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), - DriftMemoryService(ref.watch(driftProvider).memoryRepository), - ref.watch(driftPeopleServiceProvider), + MemoryService(ref.watch(driftProvider).memoryRepository), + ref.watch(peopleServiceProvider), ref.watch(currentUserProvider), ), ); @@ -31,8 +31,8 @@ class DeepLinkService { final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; - final DriftMemoryService _betaMemoryService; - final DriftPeopleService _betaPeopleService; + final MemoryService _betaMemoryService; + final PeopleService _betaPeopleService; final UserDto? _currentUser; @@ -88,7 +88,7 @@ class DeepLinkService { } Future _buildMemoryDeepLink(String? memoryId) async { - List memories = []; + List memories = []; if (memoryId == null) { if (_currentUser == null) { @@ -107,7 +107,7 @@ class DeepLinkService { return null; } - return DriftMemoryRoute(memories: memories, memoryIndex: 0); + return MemoryRoute(memories: memories, memoryIndex: 0); } Future _buildAssetDeepLink(String assetId, WidgetRef ref, {String? albumId}) async { @@ -143,7 +143,7 @@ class DeepLinkService { return null; } - return DriftActivitiesRoute(album: album); + return ActivitiesRoute(album: album); } Future _buildPeopleDeepLink(String personId) async { @@ -153,6 +153,6 @@ class DeepLinkService { return null; } - return DriftPersonRoute(person: person); + return PersonRoute(person: person); } } diff --git a/mobile/lib/utils/people.utils.dart b/mobile/lib/utils/people.utils.dart index bd2b607e0b..36c3aa8cb1 100644 --- a/mobile/lib/utils/people.utils.dart +++ b/mobile/lib/utils/people.utils.dart @@ -39,7 +39,7 @@ Future showNameEditModal(BuildContext context, Person person) { context: context, useRootNavigator: false, builder: (BuildContext context) { - return DriftPersonNameEditForm(person: person); + return PersonNameEditForm(person: person); }, ); } @@ -49,7 +49,7 @@ Future showBirthdayEditModal(BuildContext context, Person person) { context: context, useRootNavigator: false, builder: (BuildContext context) { - return DriftPersonBirthdayEditForm(person: person); + return PersonBirthdayEditForm(person: person); }, ); } diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index debb3222d9..861167e209 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -3,8 +3,8 @@ import 'package:immich_mobile/data/server/person.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; +import 'package:immich_mobile/repositories/album_api_repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; void invalidateAllApiRepositoryProviders(WidgetRef ref) { @@ -16,5 +16,5 @@ void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(searchApiRepositoryProvider); // Drift - ref.invalidate(driftAlbumApiRepositoryProvider); + ref.invalidate(albumApiRepositoryProvider); } diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart index be908259d4..069c841786 100644 --- a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -25,7 +25,7 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget { } return GestureDetector( - onTap: () => context.pushRoute(DriftAlbumOptionsRoute(album: currentAlbum)), + onTap: () => context.pushRoute(AlbumOptionsRoute(album: currentAlbum)), child: SizedBox( height: 50, child: ListView.builder( diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart similarity index 96% rename from mobile/lib/widgets/backup/drift_album_info_list_tile.dart rename to mobile/lib/widgets/backup/album_info_list_tile.dart index 41a215fb99..bcbd210c81 100644 --- a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -12,10 +12,10 @@ import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -class DriftAlbumInfoListTile extends HookConsumerWidget { +class AlbumInfoListTile extends HookConsumerWidget { final LocalAlbum album; - const DriftAlbumInfoListTile({super.key, required this.album}); + const AlbumInfoListTile({super.key, required this.album}); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index fffa6746f6..7cf9fba4fa 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -9,7 +9,7 @@ import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_server.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -29,7 +29,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - final ServerDiskInfo backupState = ref.watch(backupProvider); + final ServerDiskInfo backupState = ref.watch(backupServerProvider); final theme = context.themeData; final bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; @@ -38,7 +38,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); useEffect(() { - unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); + unawaited(ref.read(backupServerProvider.notifier).updateDiskInfo()); unawaited(ref.read(currentUserProvider.notifier).refresh()); return null; }, []); diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index bd8e883ef0..3c767f60f4 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_server.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -55,7 +55,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { unawaited(ref.read(currentUserProvider.notifier).refresh()); } - unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); + unawaited(ref.read(backupServerProvider.notifier).updateDiskInfo()); } } } diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index b966601c31..57ca74ec3c 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -9,7 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; @@ -178,8 +178,8 @@ class _BackupIndicator extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled)); - final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none)); - final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); + final hasError = ref.watch(backupProvider.select((state) => state.error != BackupError.none)); + final isUploading = ref.watch(backupProvider.select((state) => state.uploadItems.isNotEmpty)); final indicatorIcon = _getBackupBadgeIcon( context, backupEnabled: backupEnabled, @@ -188,7 +188,7 @@ class _BackupIndicator extends ConsumerWidget { ); return IconButton( - onPressed: () => context.pushRoute(const DriftBackupRoute()), + onPressed: () => context.pushRoute(const BackupRoute()), icon: Badge( label: indicatorIcon, backgroundColor: Colors.transparent, diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index 1a0a55a9ba..807309f4bb 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -90,7 +90,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))), + onPressed: () => context.pushRoute(SlideshowRoute(timeline: ref.read(timelineServiceProvider))), icon: Icon(Icons.slideshow_outlined, color: actionIconColor, shadows: actionIconShadows), ), if (currentAlbum.isActivityEnabled && currentAlbum.isShared) diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart similarity index 99% rename from mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart rename to mobile/lib/widgets/settings/backup_settings/backup_settings.dart index ae9ff87a91..b506489c45 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart @@ -18,8 +18,8 @@ import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -class DriftBackupSettings extends StatelessWidget { - const DriftBackupSettings({super.key}); +class BackupSettings extends StatelessWidget { + const BackupSettings({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index d97be93ac4..a0193dbcd9 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -223,7 +223,7 @@ class _SyncStatsCounts extends ConsumerWidget { final assetService = ref.watch(assetServiceProvider); final localAlbumService = LocalAlbumService(db.localAlbumRepository); final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); - final memoryService = DriftMemoryService(db.memoryRepository); + final memoryService = MemoryService(db.memoryRepository); final appSettingsService = ref.watch(appSettingsServiceProvider); Future> loadCounts() async { diff --git a/mobile/test/domain/services/sync_linked_album_service_test.dart b/mobile/test/domain/services/sync_linked_album_service_test.dart index 60b230ba65..9411f902ea 100644 --- a/mobile/test/domain/services/sync_linked_album_service_test.dart +++ b/mobile/test/domain/services/sync_linked_album_service_test.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/providers/infrastructure/db.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/repositories/album_api_repository.dart'; import 'package:mocktail/mocktail.dart'; import '../../infrastructure/repository.mock.dart'; @@ -20,7 +20,7 @@ void main() { final container = ProviderContainer( overrides: [ driftProvider.overrideWithValue(drift), - driftAlbumApiRepositoryProvider.overrideWithValue(MockDriftAlbumApiRepository()), + albumApiRepositoryProvider.overrideWithValue(MockAlbumApiRepository()), storeServiceProvider.overrideWithValue(MockStoreService()), ], ); diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 17ad9af46e..ce101e3c49 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -16,7 +16,7 @@ import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/repositories/album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -59,4 +59,4 @@ class MockUserApiRepository extends Mock implements UserApiRepository {} class MockSyncApiRepository extends Mock implements SyncApiRepository {} -class MockDriftAlbumApiRepository extends Mock implements DriftAlbumApiRepository {} +class MockAlbumApiRepository extends Mock implements AlbumApiRepository {} diff --git a/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart b/mobile/test/presentation/widgets/remote_album/album_option_widget_test.dart similarity index 72% rename from mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart rename to mobile/test/presentation/widgets/remote_album/album_option_widget_test.dart index 1706b4d307..ba9f698437 100644 --- a/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart +++ b/mobile/test/presentation/widgets/remote_album/album_option_widget_test.dart @@ -1,25 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; +import 'package:immich_mobile/presentation/widgets/remote_album/album_option.widget.dart'; import '../../../widget_tester_extensions.dart'; void main() { group('DriftRemoteAlbumOption', () { testWidgets('shows kebab menu icon button', (tester) async { - await tester.pumpConsumerWidget( - const DriftRemoteAlbumOption(), - ); + await tester.pumpConsumerWidget(const RemoteAlbumOption()); expect(find.byIcon(Icons.more_vert_rounded), findsOneWidget); }); testWidgets('opens menu when icon button is tapped', (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -27,15 +21,10 @@ void main() { expect(find.byIcon(Icons.edit), findsOneWidget); }); - testWidgets('shows edit album option when onEditAlbum is provided', - (tester) async { + testWidgets('shows edit album option when onEditAlbum is provided', (tester) async { bool editCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () => editCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () => editCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -48,13 +37,8 @@ void main() { expect(editCalled, isTrue); }); - testWidgets('hides edit album option when onEditAlbum is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onAddPhotos: () {}, - ), - ); + testWidgets('hides edit album option when onEditAlbum is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onAddPhotos: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -62,15 +46,10 @@ void main() { expect(find.byIcon(Icons.edit), findsNothing); }); - testWidgets('shows add photos option when onAddPhotos is provided', - (tester) async { + testWidgets('shows add photos option when onAddPhotos is provided', (tester) async { bool addPhotosCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onAddPhotos: () => addPhotosCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onAddPhotos: () => addPhotosCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -83,13 +62,8 @@ void main() { expect(addPhotosCalled, isTrue); }); - testWidgets('hides add photos option when onAddPhotos is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + testWidgets('hides add photos option when onAddPhotos is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -97,15 +71,10 @@ void main() { expect(find.byIcon(Icons.add_a_photo), findsNothing); }); - testWidgets('shows add users option when onAddUsers is provided', - (tester) async { + testWidgets('shows add users option when onAddUsers is provided', (tester) async { bool addUsersCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onAddUsers: () => addUsersCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onAddUsers: () => addUsersCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -118,13 +87,8 @@ void main() { expect(addUsersCalled, isTrue); }); - testWidgets('hides add users option when onAddUsers is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + testWidgets('hides add users option when onAddUsers is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -132,15 +96,10 @@ void main() { expect(find.byIcon(Icons.group_add), findsNothing); }); - testWidgets('shows leave album option when onLeaveAlbum is provided', - (tester) async { + testWidgets('shows leave album option when onLeaveAlbum is provided', (tester) async { bool leaveAlbumCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onLeaveAlbum: () => leaveAlbumCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onLeaveAlbum: () => leaveAlbumCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -153,13 +112,8 @@ void main() { expect(leaveAlbumCalled, isTrue); }); - testWidgets('hides leave album option when onLeaveAlbum is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + testWidgets('hides leave album option when onLeaveAlbum is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -167,16 +121,10 @@ void main() { expect(find.byIcon(Icons.person_remove_rounded), findsNothing); }); - testWidgets( - 'shows toggle album order option when onToggleAlbumOrder is provided', - (tester) async { + testWidgets('shows toggle album order option when onToggleAlbumOrder is provided', (tester) async { bool toggleOrderCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onToggleAlbumOrder: () => toggleOrderCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onToggleAlbumOrder: () => toggleOrderCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -189,13 +137,8 @@ void main() { expect(toggleOrderCalled, isTrue); }); - testWidgets('hides toggle album order option when onToggleAlbumOrder is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + testWidgets('hides toggle album order option when onToggleAlbumOrder is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -203,16 +146,10 @@ void main() { expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); }); - testWidgets( - 'shows create shared link option when onCreateSharedLink is provided', - (tester) async { + testWidgets('shows create shared link option when onCreateSharedLink is provided', (tester) async { bool createSharedLinkCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onCreateSharedLink: () => createSharedLinkCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onCreateSharedLink: () => createSharedLinkCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -225,13 +162,8 @@ void main() { expect(createSharedLinkCalled, isTrue); }); - testWidgets('hides create shared link option when onCreateSharedLink is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + testWidgets('hides create shared link option when onCreateSharedLink is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -239,15 +171,10 @@ void main() { expect(find.byIcon(Icons.link), findsNothing); }); - testWidgets('shows options option when onShowOptions is provided', - (tester) async { + testWidgets('shows options option when onShowOptions is provided', (tester) async { bool showOptionsCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onShowOptions: () => showOptionsCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onShowOptions: () => showOptionsCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -260,13 +187,8 @@ void main() { expect(showOptionsCalled, isTrue); }); - testWidgets('hides options option when onShowOptions is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + testWidgets('hides options option when onShowOptions is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -274,15 +196,10 @@ void main() { expect(find.byIcon(Icons.settings), findsNothing); }); - testWidgets('shows delete album option when onDeleteAlbum is provided', - (tester) async { + testWidgets('shows delete album option when onDeleteAlbum is provided', (tester) async { bool deleteAlbumCalled = false; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onDeleteAlbum: () => deleteAlbumCalled = true, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onDeleteAlbum: () => deleteAlbumCalled = true)); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -295,13 +212,8 @@ void main() { expect(deleteAlbumCalled, isTrue); }); - testWidgets('hides delete album option when onDeleteAlbum is null', - (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - ), - ); + testWidgets('hides delete album option when onDeleteAlbum is null', (tester) async { + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -310,12 +222,7 @@ void main() { }); testWidgets('shows divider before delete album option', (tester) async { - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onEditAlbum: () {}, - onDeleteAlbum: () {}, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onEditAlbum: () {}, onDeleteAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -323,10 +230,9 @@ void main() { expect(find.byType(Divider), findsOneWidget); }); - testWidgets('shows all options when all callbacks are provided', - (tester) async { + testWidgets('shows all options when all callbacks are provided', (tester) async { await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( + RemoteAlbumOption( onEditAlbum: () {}, onAddPhotos: () {}, onAddUsers: () {}, @@ -353,9 +259,7 @@ void main() { }); testWidgets('shows no options when all callbacks are null', (tester) async { - await tester.pumpConsumerWidget( - const DriftRemoteAlbumOption(), - ); + await tester.pumpConsumerWidget(const RemoteAlbumOption()); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); @@ -373,11 +277,7 @@ void main() { testWidgets('uses custom icon color when provided', (tester) async { const customColor = Colors.red; - await tester.pumpConsumerWidget( - const DriftRemoteAlbumOption( - iconColor: customColor, - ), - ); + await tester.pumpConsumerWidget(const RemoteAlbumOption(iconColor: customColor)); final iconButton = tester.widget(find.byType(IconButton)); final icon = iconButton.icon as Icon; @@ -385,11 +285,8 @@ void main() { expect(icon.color, equals(customColor)); }); - testWidgets('uses default white color when iconColor is null', - (tester) async { - await tester.pumpConsumerWidget( - const DriftRemoteAlbumOption(), - ); + testWidgets('uses default white color when iconColor is null', (tester) async { + await tester.pumpConsumerWidget(const RemoteAlbumOption()); final iconButton = tester.widget(find.byType(IconButton)); final icon = iconButton.icon as Icon; @@ -398,15 +295,9 @@ void main() { }); testWidgets('applies icon shadows when provided', (tester) async { - final shadows = [ - const Shadow(offset: Offset(0, 2), blurRadius: 5, color: Colors.black), - ]; + final shadows = [const Shadow(offset: Offset(0, 2), blurRadius: 5, color: Colors.black)]; - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - iconShadows: shadows, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(iconShadows: shadows)); final iconButton = tester.widget(find.byType(IconButton)); final icon = iconButton.icon as Icon; @@ -418,7 +309,7 @@ void main() { testWidgets('owner sees all management options', (tester) async { // Simulating owner scenario - all callbacks provided await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( + RemoteAlbumOption( onEditAlbum: () {}, onAddPhotos: () {}, onAddUsers: () {}, @@ -443,15 +334,10 @@ void main() { expect(find.byIcon(Icons.person_remove_rounded), findsNothing); }); - testWidgets('non-owner with editor role sees limited options', - (tester) async { + testWidgets('non-owner with editor role sees limited options', (tester) async { // Simulating non-owner with editor role - can add photos, show options, leave await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onAddPhotos: () {}, - onShowOptions: () {}, - onLeaveAlbum: () {}, - ), + RemoteAlbumOption(onAddPhotos: () {}, onShowOptions: () {}, onLeaveAlbum: () {}), ); await tester.tap(find.byIcon(Icons.more_vert_rounded)); @@ -473,12 +359,7 @@ void main() { testWidgets('non-owner viewer sees minimal options', (tester) async { // Simulating viewer - can only show options and leave - await tester.pumpConsumerWidget( - DriftRemoteAlbumOption( - onShowOptions: () {}, - onLeaveAlbum: () {}, - ), - ); + await tester.pumpConsumerWidget(RemoteAlbumOption(onShowOptions: () {}, onLeaveAlbum: () {})); await tester.tap(find.byIcon(Icons.more_vert_rounded)); await tester.pumpAndSettle(); diff --git a/mobile/test/providers/app_life_cycle_provider_test.dart b/mobile/test/providers/app_life_cycle_provider_test.dart index d4489a58ba..12db286ff4 100644 --- a/mobile/test/providers/app_life_cycle_provider_test.dart +++ b/mobile/test/providers/app_life_cycle_provider_test.dart @@ -9,7 +9,7 @@ import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; @@ -66,7 +66,7 @@ class TestWebsocketNotifier extends WebsocketNotifier { void disconnect() => disconnectCount++; } -class TestDriftBackupNotifier extends DriftBackupNotifier { +class TestDriftBackupNotifier extends BackupNotifier { TestDriftBackupNotifier() : super(MockForegroundUploadService(), MockBackgroundUploadService(), UploadSpeedManager()); } @@ -116,7 +116,7 @@ void main() { websocketProvider.overrideWith((ref) { return websocket = TestWebsocketNotifier(ref); }), - driftBackupProvider.overrideWith((_) => TestDriftBackupNotifier()), + backupProvider.overrideWith((_) => TestDriftBackupNotifier()), backgroundWorkerLockServiceProvider.overrideWithValue(lockService), ], ); diff --git a/mobile/test/providers/backup/drift_backup_provider_test.dart b/mobile/test/providers/backup/backup_provider_test.dart similarity index 94% rename from mobile/test/providers/backup/drift_backup_provider_test.dart rename to mobile/test/providers/backup/backup_provider_test.dart index 205f564cd9..791ce295db 100644 --- a/mobile/test/providers/backup/drift_backup_provider_test.dart +++ b/mobile/test/providers/backup/backup_provider_test.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/utils/upload_speed_calculator.dart'; @@ -14,7 +14,7 @@ class MockBackgroundUploadService extends Mock implements BackgroundUploadServic void main() { late MockForegroundUploadService foregroundUploadService; late MockBackgroundUploadService backgroundUploadService; - late DriftBackupNotifier notifier; + late BackupNotifier notifier; setUpAll(() { registerFallbackValue(Completer()); @@ -24,7 +24,7 @@ void main() { setUp(() { foregroundUploadService = MockForegroundUploadService(); backgroundUploadService = MockBackgroundUploadService(); - notifier = DriftBackupNotifier(foregroundUploadService, backgroundUploadService, UploadSpeedManager()); + notifier = BackupNotifier(foregroundUploadService, backgroundUploadService, UploadSpeedManager()); addTearDown(() { if (notifier.mounted) { notifier.dispose(); diff --git a/mobile/test/providers/infrastructure/memory_provider_test.dart b/mobile/test/providers/infrastructure/memory_provider_test.dart index f6e519432b..6ea3334d81 100644 --- a/mobile/test/providers/infrastructure/memory_provider_test.dart +++ b/mobile/test/providers/infrastructure/memory_provider_test.dart @@ -52,11 +52,11 @@ void main() { when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty()); }); - group('driftMemoryLaneProvider', () { + group('memoryLaneProvider', () { test('re-queries after local midnight', () { fakeAsync((async) { final container = makeContainer(); - container.listen(driftMemoryLaneProvider, (_, _) {}); + container.listen(memoryLaneProvider, (_, _) {}); async.flushMicrotasks(); verify(() => memoryRepository.getAll('user-1')).called(1); @@ -74,7 +74,7 @@ void main() { test('cancels the midnight timer when disposed', () { fakeAsync((async) { final container = makeContainer(); - final subscription = container.listen(driftMemoryLaneProvider, (_, _) {}); + final subscription = container.listen(memoryLaneProvider, (_, _) {}); async.flushMicrotasks(); verify(() => memoryRepository.getAll('user-1')).called(1); @@ -91,7 +91,7 @@ void main() { fakeAsync((async) { final container = makeContainer(); - container.listen(driftMemoryLaneProvider, (_, _) {}); + container.listen(memoryLaneProvider, (_, _) {}); async.flushMicrotasks(); async.elapse(const Duration(hours: 25)); diff --git a/mobile/test/repositories/drift_album_api_repository_test.dart b/mobile/test/repositories/album_api_repository_test.dart similarity index 94% rename from mobile/test/repositories/drift_album_api_repository_test.dart rename to mobile/test/repositories/album_api_repository_test.dart index 93c45c6687..0954a3f47d 100644 --- a/mobile/test/repositories/drift_album_api_repository_test.dart +++ b/mobile/test/repositories/album_api_repository_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/repositories/album_api_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; @@ -7,7 +7,7 @@ class _MockAlbumsApi extends Mock implements AlbumsApi {} void main() { late _MockAlbumsApi api; - late DriftAlbumApiRepository repo; + late AlbumApiRepository repo; setUpAll(() { registerFallbackValue(BulkIdsDto(ids: const [])); @@ -15,7 +15,7 @@ void main() { setUp(() { api = _MockAlbumsApi(); - repo = DriftAlbumApiRepository(api); + repo = AlbumApiRepository(api); }); void stubResponse(List response) { diff --git a/mobile/test/services/deep_link_service_test.dart b/mobile/test/services/deep_link_service_test.dart index c588cf26b4..2d07042bc0 100644 --- a/mobile/test/services/deep_link_service_test.dart +++ b/mobile/test/services/deep_link_service_test.dart @@ -22,9 +22,9 @@ class MockAssetService extends Mock implements AssetService {} class MockRemoteAlbumService extends Mock implements RemoteAlbumService {} -class MockDriftMemoryService extends Mock implements DriftMemoryService {} +class MockMemoryService extends Mock implements MemoryService {} -class MockDriftPeopleService extends Mock implements DriftPeopleService {} +class MockPeopleService extends Mock implements PeopleService {} class MockPlatformDeepLink extends Mock implements PlatformDeepLink {} @@ -62,7 +62,7 @@ final _album = RemoteAlbum( ownerName: 'Owner', ); -final _memory = DriftMemory( +final _memory = Memory( id: _memoryId, createdAt: DateTime(2026, 6, 12), updatedAt: DateTime(2026, 6, 12), @@ -85,7 +85,7 @@ void main() { late MockTimelineFactory timelineFactory; late MockAssetService assetService; late MockRemoteAlbumService remoteAlbumService; - late MockDriftMemoryService memoryService; + late MockMemoryService memoryService; late MockWidgetRef ref; late List createdTimelineServices; late DeepLinkService sut; @@ -94,7 +94,7 @@ void main() { timelineFactory = MockTimelineFactory(); assetService = MockAssetService(); remoteAlbumService = MockRemoteAlbumService(); - memoryService = MockDriftMemoryService(); + memoryService = MockMemoryService(); ref = MockWidgetRef(); createdTimelineServices = []; @@ -116,7 +116,7 @@ void main() { assetService, remoteAlbumService, memoryService, - MockDriftPeopleService(), + MockPeopleService(), _user, ); @@ -170,8 +170,8 @@ void main() { final route = await sut.handleScheme(deepLink, ref); - expect(route, isA()); - expect((route!.args! as DriftMemoryRouteArgs).memories, [_memory]); + expect(route, isA()); + expect((route!.args! as MemoryRouteArgs).memories, [_memory]); verify(() => memoryService.getMemoryLane(_userId)).called(1); verifyNever(() => memoryService.get(any())); }); @@ -183,8 +183,8 @@ void main() { final route = await sut.handleScheme(deepLink, ref); - expect(route, isA()); - expect((route!.args! as DriftMemoryRouteArgs).memories, [_memory]); + expect(route, isA()); + expect((route!.args! as MemoryRouteArgs).memories, [_memory]); verifyNever(() => memoryService.getMemoryLane(any())); verify(() => memoryService.get(_memoryId)).called(1); }); diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index ca4d5ed29b..66e2874e1b 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -31,7 +31,7 @@ class RepositoryMocks { final remoteExif = RemoteExifRepositoryStub(MockRemoteExifRepository()); final trashedAsset = MockTrashedLocalAssetRepository(); final remoteAlbum = MockRemoteAlbumRepository(); - final albumApi = MockDriftAlbumApiRepository(); + final albumApi = MockAlbumApiRepository(); final permission = PermissionRepositoryStub(MockPermissionRepository()); final nativeApi = NativeSyncApiStub(MockNativeSyncApi()); From 2c0f415a569a80c0abfaaff190ed0fe5705940ce Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 27 Aug 2026 12:30:39 +0200 Subject: [PATCH 04/42] feat: only move floating docker tags for the newest release (#31030) --- .github/workflows/docker.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 90ba3f40d6..ed1246cee4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,6 +25,8 @@ jobs: contents: read outputs: should_run: ${{ steps.check.outputs.should_run }} + highest-overall: ${{ steps.scope.outputs.highest-overall }} + highest-in-line: ${{ steps.scope.outputs.highest-in-line }} steps: - id: token uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 @@ -50,6 +52,24 @@ jobs: - '.github/workflows/docker.yml' force-events: 'workflow_dispatch,release' + - name: Determine release scope + id: scope + if: ${{ github.event_name == 'release' }} + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + REPO: ${{ github.repository }} + TAG: ${{ github.event.release.tag_name }} + PRERELEASE: ${{ github.event.release.prerelease }} + run: | + overall=$(gh api "repos/${REPO}/releases/latest" --jq '.tag_name' 2>/dev/null || true) + + releases=$(gh api "repos/${REPO}/releases" --paginate --slurp | jq 'flatten | map(select(.draft | not))') + in_line=$(jq -r --argjson pre "${PRERELEASE}" '.[] | select(.prerelease == $pre) | .tag_name' <<< "${releases}" \ + | grep -E "^${TAG%%.*}\." | sort --version-sort | tail -1) + + echo "highest-overall=$([[ "${TAG}" == "${overall}" ]] && echo true || echo false)" | tee -a "$GITHUB_OUTPUT" + echo "highest-in-line=$([[ "${TAG}" == "${in_line}" ]] && echo true || echo false)" | tee -a "$GITHUB_OUTPUT" + retag_ml: name: Re-Tag ML needs: pre-job @@ -155,9 +175,9 @@ jobs: type=ref,event=pr type=sha,format=long,prefix=commit- type=semver,pattern={{version}},prefix=v - type=semver,pattern={{major}},prefix=v - type=match,pattern=v(\d+),group=1,prefix=v,suffix=-rc,enable=${{ github.event.release.prerelease == true }} - type=raw,value=release,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }} + type=semver,pattern={{major}},prefix=v,enable=${{ needs.pre-job.outputs.highest-in-line == 'true' }} + type=match,pattern=v(\d+),group=1,prefix=v,suffix=-rc,enable=${{ github.event.release.prerelease == true && needs.pre-job.outputs.highest-in-line == 'true' }} + type=raw,value=release,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false && needs.pre-job.outputs.highest-overall == 'true' }} build-args: | DEVICE=${{ matrix.device }} @@ -180,7 +200,7 @@ jobs: mirror: name: Mirror to Docker Hub - needs: [machine-learning, server] + needs: [pre-job, machine-learning, server] if: ${{ github.event_name == 'release' && github.event.release.prerelease == false }} permissions: packages: read From c9be4c4bd377d324b06d2df84d311c988dc4783e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:45:18 +0200 Subject: [PATCH 05/42] chore(deps): update dependency github:extism/js-pdk to v1.7.0 (#30987) --- mise.lock | 44 ++++++++++++++++++++++---------------------- mise.toml | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/mise.lock b/mise.lock index 71b3985b12..fc159ab201 100644 --- a/mise.lock +++ b/mise.lock @@ -40,43 +40,43 @@ url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-wind url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035" [[tools."github:extism/js-pdk"]] -version = "1.6.0" +version = "1.7.0" backend = "github:extism/js-pdk" [tools."github:extism/js-pdk"."platforms.linux-arm64"] -checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b" -url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz" -url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214" +checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a" +url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz" +url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503" [tools."github:extism/js-pdk"."platforms.linux-arm64-musl"] -checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b" -url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz" -url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214" +checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a" +url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz" +url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503" [tools."github:extism/js-pdk"."platforms.linux-x64"] -checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68" -url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz" -url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119" +checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035" +url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz" +url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991" [tools."github:extism/js-pdk"."platforms.linux-x64-musl"] -checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68" -url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz" -url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119" +checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035" +url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz" +url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991" [tools."github:extism/js-pdk"."platforms.macos-arm64"] -checksum = "sha256:548e25bda3971a07c32d78a249135cf8cb7b3eede101e878e06e53e01ac2e0ce" -url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-macos-v1.6.0.gz" -url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223215" +checksum = "sha256:12c01c2bb2240a6a05a4f8babe680c793c397d84e6a1497a7ca1312e78a475c3" +url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-macos-v1.7.0.gz" +url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632812" [tools."github:extism/js-pdk"."platforms.macos-x64"] -checksum = "sha256:d85a875c2a071f0c29fe572764c52c3a499f157ab7f9efac8939a4364390e29b" -url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-macos-v1.6.0.gz" -url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223239" +checksum = "sha256:cce4a756eceb34b5aaac5ea864607d6c8b0610535e2c1b0c05a96bdbda69c7bb" +url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-macos-v1.7.0.gz" +url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518633618" [tools."github:extism/js-pdk"."platforms.windows-x64"] -checksum = "sha256:97b7b746141e4777e1ca2b76febdeb16dc9d314ff6a4257df05a476b67228acc" -url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-windows-v1.6.0.gz" -url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133" +checksum = "sha256:409ac023f88f79d763fbf9dd5bb5d9b2fd595ac452e0822d49bf7d05bf6794be" +url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-windows-v1.7.0.gz" +url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518636347" [[tools."github:jellyfin/jellyfin-ffmpeg"]] version = "7.1.3-6" diff --git a/mise.toml b/mise.toml index c56e6822c7..9321a226f7 100644 --- a/mise.toml +++ b/mise.toml @@ -23,7 +23,7 @@ opentofu = "1.12.5" "npm:oazapfts" = "7.5.0" "github:extism/cli" = "1.6.3" "github:webassembly/binaryen" = "version_124" -"github:extism/js-pdk" = "1.6.0" +"github:extism/js-pdk" = "v1.7.0" java = "21.0.2" [tools."github:jellyfin/jellyfin-ffmpeg"] From 6555fb29edc96762a560f8ecaae57dac29f2212b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:45:46 +0200 Subject: [PATCH 06/42] chore(deps): update grafana/grafana docker tag to v12.4.9-ubuntu (#30986) --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 660ad73732..0881fbc7c7 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -97,7 +97,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.4.8-ubuntu@sha256:473cfc1712694135a5d9dc012dc573f2a48e51efb9ac0da50418918f3cbd8ad3 + image: grafana/grafana:12.4.9-ubuntu@sha256:91dd82bc25850f41da5bfd769531d94ec8352a818fc84bf7f2ee8b7389e09971 volumes: - grafana-data:/var/lib/grafana From de04e651b46215b45fc22c6de9ffd122cb5ffe35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:46:06 +0200 Subject: [PATCH 07/42] chore(deps): update prom/prometheus docker digest to 5ce7540 (#30981) --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 0881fbc7c7..048287553b 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -85,7 +85,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:508729e0e2d18e11fd742a5a5ca70e557b940a93948c3c95fd0123a6fd538b69 + image: prom/prometheus@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From f46e107d167d6ae2cb9ef8c47bf4b6c5de87dfe1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:59:03 +0000 Subject: [PATCH 08/42] chore(deps): update dependency opentofu to v1.12.6 (#30984) --- deployment/mise.toml | 2 +- mise.lock | 44 ++++++++++++++++++++++---------------------- mise.toml | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/deployment/mise.toml b/deployment/mise.toml index 7098b79c99..a2c763c91e 100644 --- a/deployment/mise.toml +++ b/deployment/mise.toml @@ -1,6 +1,6 @@ [tools] terragrunt = "1.1.1" -opentofu = "1.12.5" +opentofu = "1.12.6" [tasks."tg:fmt"] run = "terragrunt hclfmt" diff --git a/mise.lock b/mise.lock index fc159ab201..74493ebadd 100644 --- a/mise.lock +++ b/mise.lock @@ -252,43 +252,43 @@ version = "7.5.0" backend = "npm:oazapfts" [[tools.opentofu]] -version = "1.12.5" +version = "1.12.6" backend = "aqua:opentofu/opentofu" [tools.opentofu."platforms.linux-arm64"] -checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" +checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688" [tools.opentofu."platforms.linux-arm64-musl"] -checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" +checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688" [tools.opentofu."platforms.linux-x64"] -checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" +checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723" [tools.opentofu."platforms.linux-x64-musl"] -checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" +checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723" [tools.opentofu."platforms.macos-arm64"] -checksum = "sha256:2ae38150a667f5c0bd57b318d18ad8091d08f93fcca40345f3d88998661de5a9" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602544" +checksum = "sha256:f958ec5e511063be9feb180ca015a4cb7977566a9cf6a8550bba8c2a9b5aba74" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762710" [tools.opentofu."platforms.macos-x64"] -checksum = "sha256:1012d8f3d4567bcbcd1f2c7d766feca39a30bced32fb8be47e1887fbbee2456d" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602621" +checksum = "sha256:44bb1855f372f17f365fb94517906e78da5001da10f4c98de57a39bf982f3a92" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762655" [tools.opentofu."platforms.windows-x64"] -checksum = "sha256:af11850b496f3720e0184084c56d8b43aa74ea92d2338978bf368d70c96473f1" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_windows_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" +checksum = "sha256:bcbcd9ca744af6c4053e2d5e75dae79ff33ad7dc38d45e1233e693d096284538" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_windows_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762708" [[tools.pnpm]] version = "11.22.0" diff --git a/mise.toml b/mise.toml index 9321a226f7..93ecd4196e 100644 --- a/mise.toml +++ b/mise.toml @@ -18,7 +18,7 @@ config_roots = [ node = "24.15.0" pnpm = "11.22.0" terragrunt = "1.1.1" -opentofu = "1.12.5" +opentofu = "1.12.6" "npm:@openapitools/openapi-generator-cli" = "2.40.1" "npm:oazapfts" = "7.5.0" "github:extism/cli" = "1.6.3" From 05e84dd5db78fce145a70623875a5639d5d409a0 Mon Sep 17 00:00:00 2001 From: thejeff77 Date: Thu, 27 Aug 2026 05:47:09 -0600 Subject: [PATCH 09/42] fix(web): restore timeline scroll when the asset id cannot be resolved (#30916) Co-authored-by: Daniel Dietzler Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- web/src/lib/components/timeline/Timeline.svelte | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index 1fe6c7b097..a98e2beb89 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -210,13 +210,13 @@ } const scrollTarget = assetViewerManager.gridScrollTarget?.at; const scrolled = scrollTarget ? await scrollAndLoadAsset(scrollTarget) : false; - if (!scrolled) { - // if the asset is not found, scroll to the top - timelineManager.scrollTo(0); - } else if (scrollTarget) { + if (scrolled && scrollTarget) { await tick(); focusAsset(scrollTarget); + } else { + timelineManager.scrollTo(lastVisibleScrollTop); } + invisible = false; }; @@ -224,6 +224,9 @@ let initialLoadWasAssetViewer: boolean | null = null; // only modified in beforeNavigate() let hasNavigatedToOrFromAssetViewer: boolean = false; + // The layout sets `display: none` on this subtree while the viewer is open. + // Browsers drop the scroll offset of a hidden element. + let lastVisibleScrollTop = 0; // beforeNavigate is only called AFTER a svelte route has already been loaded // and a new route is being navigated to. It will never be called on direct @@ -309,6 +312,9 @@ if (!scrollableElement) { return; } + if (!assetViewerManager.isViewing) { + lastVisibleScrollTop = scrollableElement.scrollTop; + } if (timelineManager.limitedScroll) { // edge case - scroll limited due to size of content, must adjust - use the overall percent instead From 293dd5889c8d3051d1f6ec72268a2703a59c8add Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 27 Aug 2026 14:44:49 +0200 Subject: [PATCH 10/42] chore: bump openapi-generator version to v7.25 (#30995) --- open-api/openapitools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open-api/openapitools.json b/open-api/openapitools.json index 936223a5d8..8dd0d74557 100644 --- a/open-api/openapitools.json +++ b/open-api/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.24.0" + "version": "7.25.0" } } From 0179e612a10e3f6a660a38d13afacf2e015e22ec Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 27 Aug 2026 15:10:06 +0200 Subject: [PATCH 11/42] feat: split release preparation from drafting (#31024) --- .github/workflows/draft-release.yml | 123 ++++++++++++++++++++++++++ .github/workflows/prepare-release.yml | 100 ++------------------- 2 files changed, 132 insertions(+), 91 deletions(-) create mode 100644 .github/workflows/draft-release.yml diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml new file mode 100644 index 0000000000..5b054095a5 --- /dev/null +++ b/.github/workflows/draft-release.yml @@ -0,0 +1,123 @@ +name: Draft new release + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: {} + +jobs: + tag: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + rc: ${{ steps.version.outputs.rc }} + sha: ${{ steps.version.outputs.sha }} + permissions: {} + steps: + - id: token + uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 + with: + client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + permission-contents: write + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + token: ${{ steps.token.outputs.token }} + persist-credentials: true + + - id: version + run: | + version="v$(jq -r '.version' package.json)" + echo "version=${version}" | tee -a "$GITHUB_OUTPUT" + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + if [[ "${version}" =~ -rc\.[0-9]+$ ]]; then + echo "rc=true" >> "$GITHUB_OUTPUT" + else + echo "rc=false" >> "$GITHUB_OUTPUT" + fi + + - name: Push tag + env: + TAG: ${{ steps.version.outputs.version }} + run: | + git tag "${TAG}" + git push origin "${TAG}" + + build_mobile: + uses: ./.github/workflows/build-mobile.yml + needs: tag + permissions: + contents: read + pull-requests: write + secrets: + PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} + PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + KEY_JKS: ${{ secrets.KEY_JKS }} + ALIAS: ${{ secrets.ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + # iOS secrets + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} + APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} + IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} + IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} + + with: + ref: ${{ needs.tag.outputs.sha }} + environment: production + + draft_release: + runs-on: ubuntu-latest + needs: [tag, build_mobile] + permissions: {} + steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + permission-contents: write + permission-actions: read + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + token: ${{ steps.generate-token.outputs.token }} + persist-credentials: false + + - name: Download APK + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-apk-signed + github-token: ${{ steps.generate-token.outputs.token }} + + - name: Create draft release + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + REPO: ${{ github.repository }} + TAG: ${{ needs.tag.outputs.version }} + PRERELEASE: ${{ needs.tag.outputs.rc }} + run: | + gh release create "${TAG}" \ + --repo "${REPO}" \ + --draft \ + --verify-tag \ + --prerelease="${PRERELEASE}" \ + --notes-file misc/release/notes.tmpl \ + --generate-notes \ + docker/docker-compose.yml \ + docker/docker-compose.rootless.yml \ + docker/example.env \ + docker/hwaccel.ml.yml \ + docker/hwaccel.transcoding.yml \ + docker/prometheus.yml \ + *.apk diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 6b43724745..aa26603f10 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -44,10 +44,8 @@ jobs: runs-on: ubuntu-latest needs: [merge_translations] outputs: - ref: ${{ steps.push-tag.outputs.commit_long_sha }} version: ${{ steps.output.outputs.version }} - rc: ${{ steps.output.outputs.rc }} - permissions: {} # No job-level permissions are needed because it uses the app-token + permissions: {} steps: - id: token uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 @@ -78,94 +76,14 @@ jobs: run: mise //:release --type "${RELEASE_TYPE}" - id: output - run: | - echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT - if [[ "$IMMICH_VERSION" =~ -rc\.[0-9]+$ ]]; then - echo "rc=true" >> $GITHUB_OUTPUT - else - echo "rc=false" >> $GITHUB_OUTPUT - fi + run: echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT - - name: Commit and tag - id: push-tag - uses: EndBug/add-and-commit@cc9c08ba6c8df3b93a8f2db63e89b98368ae2ae8 # v11.1.1 - with: - default_author: github_actions - message: 'chore: version ${{ steps.output.outputs.version }}' - tag: ${{ steps.output.outputs.version }} - push: true - - build_mobile: - uses: ./.github/workflows/build-mobile.yml - needs: bump_version - permissions: - contents: read - pull-requests: write - secrets: - PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} - PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - KEY_JKS: ${{ secrets.KEY_JKS }} - ALIAS: ${{ secrets.ALIAS }} - ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} - # iOS secrets - APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} - APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} - IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} - IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} - FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} - - with: - ref: ${{ needs.bump_version.outputs.ref }} - environment: production - - prepare_release: - runs-on: ubuntu-latest - needs: [build_mobile, bump_version] - permissions: - actions: read # To download the app artifact - # No content permissions are needed because it uses the app-token - steps: - - name: Generate a token - id: generate-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} - private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - permission-contents: write - permission-actions: read - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - token: ${{ steps.generate-token.outputs.token }} - persist-credentials: false - - - name: Download APK - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-apk-signed - github-token: ${{ steps.generate-token.outputs.token }} - - - name: Create draft release + - name: Commit env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - REPO: ${{ github.repository }} - TAG: ${{ needs.bump_version.outputs.version }} - PRERELEASE: ${{ needs.bump_version.outputs.rc }} + VERSION: ${{ steps.output.outputs.version }} run: | - gh release create "${TAG}" \ - --repo "${REPO}" \ - --draft \ - --verify-tag \ - --prerelease="${PRERELEASE}" \ - --notes-file misc/release/notes.tmpl \ - --generate-notes \ - docker/docker-compose.yml \ - docker/docker-compose.rootless.yml \ - docker/example.env \ - docker/hwaccel.ml.yml \ - docker/hwaccel.transcoding.yml \ - docker/prometheus.yml \ - *.apk + git config user.name 'immich-push-o-matic[bot]' + git config user.email '179150890+immich-push-o-matic[bot]@users.noreply.github.com' + git add -A + git commit -m "chore: version ${VERSION}" + git push From 3be6b122922982126029e962a12afa8f7fd9839a Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 27 Aug 2026 15:10:07 +0200 Subject: [PATCH 12/42] feat: gate release publishing behind an approval (#31025) --- .github/workflows/draft-release.yml | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 5b054095a5..a8cebb7cd9 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -121,3 +121,43 @@ jobs: docker/hwaccel.transcoding.yml \ docker/prometheus.yml \ *.apk + + publish: + runs-on: ubuntu-latest + needs: [tag, draft_release] + environment: release + permissions: {} + steps: + - id: token + uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 + with: + client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + permission-contents: write + + - name: Publish release + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + REPO: ${{ github.repository }} + TAG: ${{ needs.tag.outputs.version }} + PRERELEASE: ${{ needs.tag.outputs.rc }} + run: | + previous=$(gh api "repos/${REPO}/releases/latest" --jq '.tag_name' 2>/dev/null || true) + # version-sort the two and see which wins: if it is TAG, it supersedes the current latest + newest=$(printf '%s\n%s\n' "${previous}" "${TAG}" | sort --version-sort | tail -1) + + if [[ "${PRERELEASE}" == "true" ]]; then + latest=false + elif [[ -z "${previous}" || "${newest}" == "${TAG}" ]]; then + latest=true + else + # an older release line, published after a newer one + latest=false + fi + + echo "publishing ${TAG} (latest=${latest}, prerelease=${PRERELEASE}, previous=${previous:-none})" + gh release edit "${TAG}" \ + --repo "${REPO}" \ + --draft=false \ + --latest="${latest}" \ + --prerelease="${PRERELEASE}" From a366db1494f0fb62dc0050c5d10662a2d8878517 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 27 Aug 2026 15:10:07 +0200 Subject: [PATCH 13/42] feat: cut releases from release branches (#31033) --- .github/workflows/build-mobile.yml | 25 +++++++-------- .github/workflows/cli.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/docs-build.yml | 2 +- .github/workflows/draft-release.yml | 12 +++++++ .github/workflows/migration-order.yml | 3 +- .github/workflows/prepare-release.yml | 32 +++++++++++++++---- .github/workflows/static_analysis.yml | 2 +- .github/workflows/test.yml | 2 +- packages/scripts/src/commands/release.spec.ts | 9 ++++-- packages/scripts/src/commands/release.ts | 11 +++++-- 11 files changed, 70 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 6a9a01d41d..68744fada6 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -38,7 +38,7 @@ on: required: true pull_request: push: - branches: [main] + branches: [main, 'release/*'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -46,6 +46,9 @@ concurrency: permissions: {} +env: + DEPLOY: ${{ inputs.environment == 'production' || github.ref == 'refs/heads/main' }} + jobs: pre-job: runs-on: ubuntu-latest @@ -143,10 +146,9 @@ jobs: ALIAS: ${{ secrets.ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} - IS_MAIN: ${{ github.ref == 'refs/heads/main' }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | - if [[ $IS_MAIN == 'true' ]]; then + if [[ $DEPLOY == 'true' ]]; then flutter build apk --release flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 else @@ -229,7 +231,7 @@ jobs: uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 with: ruby-version: '3.3' - bundler-cache: true + bundler-cache: true # zizmor: ignore[cache-poisoning] release branches cannot read caches written by pull requests working-directory: ./mobile/ios - name: Install dependencies @@ -288,21 +290,16 @@ jobs: APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} ENVIRONMENT: ${{ inputs.environment || 'development' }} - GITHUB_REF: ${{ github.ref }} FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6 working-directory: ./mobile/ios run: | - # Only upload to TestFlight on main branch - if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then - if [[ "$ENVIRONMENT" == "development" ]]; then - bundle exec fastlane gha_testflight_dev - else - bundle exec fastlane gha_release_prod - fi - else - # Build only, no TestFlight upload for non-main branches + if [[ "$DEPLOY" != 'true' ]]; then bundle exec fastlane gha_build_only + elif [[ "$ENVIRONMENT" == "development" ]]; then + bundle exec fastlane gha_testflight_dev + else + bundle exec fastlane gha_release_prod fi - name: Clean up keychain diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index cb20a017fa..abc075259b 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -1,7 +1,7 @@ name: CLI Build on: push: - branches: [main] + branches: [main, 'release/*'] paths: - 'packages/cli/**' - '.github/workflows/cli.yml' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ed1246cee4..18ffcb2369 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,7 +3,7 @@ name: Docker on: workflow_dispatch: push: - branches: [main] + branches: [main, 'release/*'] pull_request: release: types: [published] diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index e0c5de3fab..e7911d97f4 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -1,7 +1,7 @@ name: Docs build on: push: - branches: [main] + branches: [main, 'release/*'] pull_request: release: types: [published] diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index a8cebb7cd9..78f7a57fe9 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -18,6 +18,18 @@ jobs: sha: ${{ steps.version.outputs.sha }} permissions: {} steps: + - name: Check this is a release branch + env: + BRANCH: ${{ github.ref_name }} + run: | + case "${BRANCH}" in + release/*) ;; + *) + echo "::error::releases are drafted from a release branch, not ${BRANCH}" + exit 1 + ;; + esac + - id: token uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 with: diff --git a/.github/workflows/migration-order.yml b/.github/workflows/migration-order.yml index 94708b4869..9975c87e21 100644 --- a/.github/workflows/migration-order.yml +++ b/.github/workflows/migration-order.yml @@ -3,7 +3,7 @@ name: Migration Order on: pull_request: push: - branches: [main] + branches: [main, 'release/*'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -14,6 +14,7 @@ permissions: {} jobs: migration-order: name: Verify migration ORDER + if: ${{ !github.event.created }} runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index aa26603f10..0fef2ac241 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -6,15 +6,12 @@ on: releaseType: description: 'Release type' required: true - default: 'false' + default: 'preminor' type: choice options: - - 'false' - - minor - - patch - premajor - preminor - - prepatch + - patch - prerelease - release skipTranslations: @@ -32,7 +29,7 @@ jobs: merge_translations: uses: ./.github/workflows/merge-translations.yml with: - skip: ${{ inputs.skipTranslations }} + skip: ${{ inputs.skipTranslations || github.ref_name != 'main' }} permissions: pull-requests: write secrets: @@ -47,6 +44,20 @@ jobs: version: ${{ steps.output.outputs.version }} permissions: {} steps: + - name: Check the release type suits this branch + env: + BRANCH: ${{ github.ref_name }} + RELEASE_TYPE: ${{ inputs.releaseType }} + run: | + case "${BRANCH}:${RELEASE_TYPE}" in + main:premajor | main:preminor) ;; + release/*:patch | release/*:prerelease | release/*:release) ;; + *) + echo "::error::${RELEASE_TYPE} cannot be released from ${BRANCH}" + exit 1 + ;; + esac + - id: token uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 with: @@ -59,7 +70,6 @@ jobs: with: token: ${{ steps.token.outputs.token }} persist-credentials: true - ref: main - name: Setup Mise uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 @@ -87,3 +97,11 @@ jobs: git add -A git commit -m "chore: version ${VERSION}" git push + + - name: Cut the release branch + if: ${{ github.ref_name == 'main' }} + env: + VERSION: ${{ steps.output.outputs.version }} + run: | + line=$(sed -E 's/^(v[0-9]+\.[0-9]+).*/\1/' <<< "${VERSION}") + git push origin "HEAD:refs/heads/release/${line}" diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 0a3c61668c..e53375f64a 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: pull_request: push: - branches: [main] + branches: [main, 'release/*'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a4ecceb14..5da8fd0002 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: pull_request: push: - branches: [main] + branches: [main, 'release/*'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/packages/scripts/src/commands/release.spec.ts b/packages/scripts/src/commands/release.spec.ts index dc3f82517d..76931edcd4 100644 --- a/packages/scripts/src/commands/release.spec.ts +++ b/packages/scripts/src/commands/release.spec.ts @@ -32,6 +32,13 @@ describe(getNewVersion.name, () => { ['release', '2.8.0-rc.1', '2.8.0'], ], }, + { + name: 'cutting the next line while on a prerelease', + items: [ + ['preminor', '3.3.0-rc.0', '3.4.0-rc.0'], + ['premajor', '3.3.0-rc.0', '4.0.0-rc.0'], + ], + }, { name: 'premajor', items: [ @@ -58,8 +65,6 @@ describe(getNewVersion.name, () => { ['patch', 'v3.0.0-rc.0'], ['prepatch', 'v3.0.0-rc.0'], ['minor', 'v3.0.0-rc.0'], - ['preminor', 'v3.0.0-rc.0'], - ['premajor', 'v3.0.0-rc.0'], ['prerelease', 'v3.0.0'], ['release', 'v3.0.0'], ])('should not allow a $0 on $1', (type, version) => { diff --git a/packages/scripts/src/commands/release.ts b/packages/scripts/src/commands/release.ts index 2c0d82d5f6..78e5027bd7 100644 --- a/packages/scripts/src/commands/release.ts +++ b/packages/scripts/src/commands/release.ts @@ -123,15 +123,20 @@ export const getNewVersion = (versionRaw: string, type: string) => { switch (type) { case 'patch': case 'prepatch': - case 'minor': - case 'preminor': - case 'premajor': { + case 'minor': { newVersionRaw = inc(version, type); // can only use while not in a prerelease valid = !isPrerelease(version); break; } + case 'preminor': + case 'premajor': { + // cutting the next release line is allowed while main sits on a prerelease + newVersionRaw = inc(version, type); + break; + } + case 'prerelease': { newVersionRaw = inc(version, type); // can only use while in a prerelease From b22483de33635db573b865ed37bd143b1553e262 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 27 Aug 2026 15:10:07 +0200 Subject: [PATCH 14/42] feat: backport pull requests to release branches (#31035) --- .github/workflows/backport.yml | 47 +++++++++++++++++++++++++++ .github/workflows/draft-release.yml | 22 +++++++++++++ .github/workflows/migration-order.yml | 15 +++++++++ .github/workflows/prepare-release.yml | 5 +++ 4 files changed, 89 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000..e14f506c46 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,47 @@ +name: Backport + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] no pull request code is checked out or executed + types: [closed, labeled] + +permissions: {} + +jobs: + backport: + name: Backport pull request + if: ${{ github.event.pull_request.merged }} + runs-on: ubuntu-latest + permissions: {} + steps: + - id: token + uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 + with: + client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + permission-contents: write + permission-pull-requests: write + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + token: ${{ steps.token.outputs.token }} + persist-credentials: true + + - name: Create backport pull requests + uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6.0 + with: + github_token: ${{ steps.token.outputs.token }} + label_pattern: '^backport:(release/.+)$' + add_team_reviewers: team + auto_merge_enabled: true + auto_merge_method: squash + comment_style: summary + copy_all_reviewers: true + copy_labels_pattern: '^changelog:' + experimental: | + { + "conflict_resolution": "draft_commit_conflicts" + } + pull_title: '${pull_title}' + git_committer_name: 'immich-push-o-matic[bot]' + git_committer_email: '179150890+immich-push-o-matic[bot]@users.noreply.github.com' diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 78f7a57fe9..319e8a4b3a 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -54,6 +54,28 @@ jobs: echo "rc=false" >> "$GITHUB_OUTPUT" fi + - name: Checkout main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + path: main + persist-credentials: false + token: ${{ steps.token.outputs.token }} + + - name: Setup Mise + uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1 + with: + github_token: ${{ steps.token.outputs.token }} + + - name: Install dependencies + run: mise //server:install + + - name: Verify the release line does not diverge from main + env: + MAIN_MIGRATIONS: ${{ github.workspace }}/main/server/src/schema/migrations + ORDER: ${{ github.workspace }}/server/src/schema/migrations/ORDER + run: mise //server:migrations verify-order --source-folder "${MAIN_MIGRATIONS}" --append-only-from "${ORDER}" + - name: Push tag env: TAG: ${{ steps.version.outputs.version }} diff --git a/.github/workflows/migration-order.yml b/.github/workflows/migration-order.yml index 9975c87e21..7cf39ae5bf 100644 --- a/.github/workflows/migration-order.yml +++ b/.github/workflows/migration-order.yml @@ -21,6 +21,8 @@ jobs: env: BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} BASELINE: ${{ github.workspace }}/base/server/src/schema/migrations/ORDER + MAIN_MIGRATIONS: ${{ github.workspace }}/main/server/src/schema/migrations + ORDER: ${{ github.workspace }}/server/src/schema/migrations/ORDER steps: - id: token uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0 @@ -53,3 +55,16 @@ jobs: - name: Verify migration ORDER run: mise //server:migrations verify-order --append-only-from "${BASELINE}" + + - name: Checkout main + if: ${{ startsWith(github.base_ref, 'release/') || startsWith(github.ref_name, 'release/') }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + path: main + persist-credentials: false + token: ${{ steps.token.outputs.token }} + + - name: Verify the release line does not diverge from main + if: ${{ startsWith(github.base_ref, 'release/') || startsWith(github.ref_name, 'release/') }} + run: mise //server:migrations verify-order --source-folder "${MAIN_MIGRATIONS}" --append-only-from "${ORDER}" diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 0fef2ac241..c0d8f2c310 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -64,6 +64,7 @@ jobs: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} permission-contents: write + permission-issues: write - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -101,7 +102,11 @@ jobs: - name: Cut the release branch if: ${{ github.ref_name == 'main' }} env: + GH_TOKEN: ${{ steps.token.outputs.token }} VERSION: ${{ steps.output.outputs.version }} run: | line=$(sed -E 's/^(v[0-9]+\.[0-9]+).*/\1/' <<< "${VERSION}") git push origin "HEAD:refs/heads/release/${line}" + gh label create "backport:release/${line}" --force \ + --description "Backport this pull request to release/${line}" \ + --color 5319e7 From 6eb1c93882db5ce8cd68f9e2b09f1db89b1669d4 Mon Sep 17 00:00:00 2001 From: Zlendy Date: Thu, 27 Aug 2026 15:27:27 +0200 Subject: [PATCH 15/42] fix(web): do not play memory video if the `MemoryVideoViewer` element is not visible (#30947) --- .../[id]/[[photos=photos]]/[[assetId=id]]/MemoryViewer.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/routes/(user)/memories/[id]/[[photos=photos]]/[[assetId=id]]/MemoryViewer.svelte b/web/src/routes/(user)/memories/[id]/[[photos=photos]]/[[assetId=id]]/MemoryViewer.svelte index bee691c4ff..2ee2023a4f 100644 --- a/web/src/routes/(user)/memories/[id]/[[photos=photos]]/[[assetId=id]]/MemoryViewer.svelte +++ b/web/src/routes/(user)/memories/[id]/[[photos=photos]]/[[assetId=id]]/MemoryViewer.svelte @@ -172,7 +172,8 @@ galleryInView = false; // only call play after the first page load. When page first loads the gallery will not be visible // and calling play here will result in duplicate invocation. - if (!galleryFirstLoad) { + // also check if the element is visible to avoid playback in the background + if (!galleryFirstLoad && videoPlayer?.checkVisibility()) { handlePromiseError(handleAction('galleryOutOfView', 'play')); } galleryFirstLoad = false; From c7d24321bf30ba83ebb4d4ae43bf9742c91b847f Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:38:07 +0200 Subject: [PATCH 16/42] feat: allow users to re-run facial recognition for their group (#30965) --- i18n/en.json | 2 + mise.lock | 34 ++++---- open-api/immich-openapi-specs.json | 45 +++++++++++ packages/sdk/src/fetch-client.ts | 11 +++ .../controllers/cluster-group.controller.ts | 12 +++ server/src/queries/person.repository.sql | 33 +++++++- server/src/repositories/person.repository.ts | 23 +++++- server/src/services/cluster-group.service.ts | 11 ++- server/src/services/person.service.spec.ts | 2 +- server/src/services/person.service.ts | 13 ++- server/src/types.ts | 6 +- .../specs/services/person.service.spec.ts | 80 ++++++++++++++++++- .../user-settings/SharingSettings.svelte | 28 ++++++- 13 files changed, 268 insertions(+), 32 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 3af360cf8e..4b1e0945b3 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -740,6 +740,8 @@ "close": "Close", "cluster_group": "Cluster group", "cluster_group_description": "People are recognized across the photos of everyone in the group", + "cluster_group_facial_recognition": "Reset facial recognition", + "cluster_group_facial_recognition_prompt": "Resetting facial recognition will delete all people for all users in this group. Already set names and birth dates will be lost. Are you sure you want to continue?", "cluster_group_invite_description": "You have been invited to join this cluster group.", "collapse": "Collapse", "collapse_all": "Collapse all", diff --git a/mise.lock b/mise.lock index 74493ebadd..5a55fd40ee 100644 --- a/mise.lock +++ b/mise.lock @@ -91,6 +91,23 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" +[tools."github:jellyfin/jellyfin-ffmpeg".options] +asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" + +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"] +checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" + +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"] +checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" + +[[tools."github:jellyfin/jellyfin-ffmpeg"]] +version = "7.1.3-6" +backend = "github:jellyfin/jellyfin-ffmpeg" + [tools."github:jellyfin/jellyfin-ffmpeg".options] asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" @@ -120,23 +137,6 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" -[tools."github:jellyfin/jellyfin-ffmpeg".options] -asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" - -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"] -checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" - -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"] -checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" - -[[tools."github:jellyfin/jellyfin-ffmpeg"]] -version = "7.1.3-6" -backend = "github:jellyfin/jellyfin-ffmpeg" - [tools."github:jellyfin/jellyfin-ffmpeg".options] asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b2bd309f61..fea598e604 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6298,6 +6298,51 @@ "x-immich-permission": "clusterGroup.leave" } }, + "/cluster-groups/{id}/regenerate-people": { + "post": { + "description": "Forcefully re-run facial recognition for all faces of users in this group.", + "operationId": "clusterGroupRegeneratePeople", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Regenerate people of users in cluster group", + "tags": [ + "Cluster groups" + ], + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + } + ], + "x-immich-permission": "clusterGroup.read" + } + }, "/cluster-groups/{id}/requests": { "get": { "description": "Retrieve the pending requests for other users to join the cluster group.", diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 722ed1a259..99166a7b66 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -5016,6 +5016,17 @@ export function leaveClusterGroup({ id }: { method: "POST" })); } +/** + * Regenerate people of users in cluster group + */ +export function clusterGroupRegeneratePeople({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/cluster-groups/${encodeURIComponent(id)}/regenerate-people`, { + ...opts, + method: "POST" + })); +} /** * Retrieve the requests sent by a cluster group */ diff --git a/server/src/controllers/cluster-group.controller.ts b/server/src/controllers/cluster-group.controller.ts index 58d7fa5d08..8df0cc9a4b 100644 --- a/server/src/controllers/cluster-group.controller.ts +++ b/server/src/controllers/cluster-group.controller.ts @@ -93,6 +93,18 @@ export class ClusterGroupController { return value; } + @Post(':id/regenerate-people') + @Authenticated({ permission: Permission.ClusterGroupRead }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Regenerate people of users in cluster group', + description: 'Forcefully re-run facial recognition for all faces of users in this group.', + history: new HistoryBuilder().added('v3.2.0'), + }) + clusterGroupRegeneratePeople(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.regeneratePeople(auth, id); + } + @Post(':id/leave') @Authenticated({ permission: Permission.ClusterGroupLeave }) @HttpCode(HttpStatus.NO_CONTENT) diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index aa17fd05fa..8f07b91800 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -7,6 +7,18 @@ set where "asset_face"."personGroupId" = $2 +-- PersonRepository.unassignFaces +update "asset_face" +set + "personGroupId" = $1 +from + "asset" + inner join "user" on "user"."id" = "asset"."ownerId" +where + "asset_face"."assetId" = "asset"."id" + and "asset_face"."sourceType" = $2 + and "user"."clusterGroupId" = $3 + -- PersonRepository.delete delete from "person" where @@ -46,6 +58,20 @@ where "user"."clusterGroupId" = "cluster_group"."id" ) +-- PersonRepository.getAllFaces +select + "asset_face".* +from + "asset_face" + inner join "asset" on "asset"."id" = "asset_face"."assetId" + inner join "user" on "user"."id" = "asset"."ownerId" +where + "asset_face"."personGroupId" is null + and "asset_face"."sourceType" = $1 + and "user"."clusterGroupId" = $2 + and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true + -- PersonRepository.getFileSamples select "ownerId", @@ -112,12 +138,15 @@ from left join "asset_face" on "asset_face"."personGroupId" = "person"."personGroupId" where "asset_face"."deletedAt" is null - and "asset_face"."isVisible" is true + and ( + "asset_face"."isVisible" is null + or "asset_face"."isVisible" = $1 + ) group by "person"."ownerId", "person"."personGroupId" having - count("asset_face"."assetId") = $1 + count("asset_face"."assetId") = $2 -- PersonRepository.getFaces select diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 28aea85041..caa3d79e52 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -58,9 +58,10 @@ export interface GetAllFacesOptions { personGroupId?: string | null; assetId?: string; sourceType?: SourceType; + clusterGroupId?: string; } -export type UnassignFacesOptions = DeleteFacesOptions; +export type UnassignFacesOptions = DeleteFacesOptions & { clusterGroupId?: string }; export type GetFacesOptions = WithPersonOptions & { isVisible?: boolean }; @@ -112,11 +113,17 @@ export class PersonRepository { return Number(result.numChangedRows ?? 0); } - async unassignFaces({ sourceType }: UnassignFacesOptions): Promise { + @GenerateSql({ params: [{ sourceType: SourceType.MachineLearning, clusterGroupId: DummyValue.UUID }] }) + async unassignFaces({ sourceType, clusterGroupId }: UnassignFacesOptions): Promise { await this.db .updateTable('asset_face') .set({ personGroupId: null }) + .from('asset') + .whereRef('asset_face.assetId', '=', 'asset.id') .where('asset_face.sourceType', '=', sourceType) + .$if(!!clusterGroupId, (qb) => + qb.innerJoin('user', 'user.id', 'asset.ownerId').where('user.clusterGroupId', '=', clusterGroupId!), + ) .execute(); } @@ -179,6 +186,10 @@ export class PersonRepository { await this.db.deleteFrom('asset_face').where('asset_face.sourceType', '=', sourceType).execute(); } + @GenerateSql({ + params: [{ personGroupId: null, sourceType: SourceType.MachineLearning, clusterGroupId: DummyValue.UUID }], + stream: true, + }) getAllFaces(options: GetAllFacesOptions = {}) { return this.db .selectFrom('asset_face') @@ -187,6 +198,12 @@ export class PersonRepository { .$if(!!options.personGroupId, (qb) => qb.where('asset_face.personGroupId', '=', options.personGroupId!)) .$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!)) .$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!)) + .$if(!!options.clusterGroupId, (qb) => + qb + .innerJoin('asset', 'asset.id', 'asset_face.assetId') + .innerJoin('user', 'user.id', 'asset.ownerId') + .where('user.clusterGroupId', '=', options.clusterGroupId!), + ) .where('asset_face.deletedAt', 'is', null) .where('asset_face.isVisible', 'is', true) .stream(); @@ -288,7 +305,7 @@ export class PersonRepository { .selectAll('person') .leftJoin('asset_face', 'asset_face.personGroupId', 'person.personGroupId') .where('asset_face.deletedAt', 'is', null) - .where('asset_face.isVisible', 'is', true) + .where((eb) => eb.or([eb('asset_face.isVisible', 'is', null), eb('asset_face.isVisible', '=', true)])) .having((eb) => eb.fn.count('asset_face.assetId'), '=', 0) .groupBy(['person.ownerId', 'person.personGroupId']) .execute(); diff --git a/server/src/services/cluster-group.service.ts b/server/src/services/cluster-group.service.ts index bff5fdf7dd..521c2b0a51 100644 --- a/server/src/services/cluster-group.service.ts +++ b/server/src/services/cluster-group.service.ts @@ -7,7 +7,7 @@ import { mapClusterGroupRequest, } from 'src/dtos/cluster-group.dto'; import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { Permission } from 'src/enum'; +import { JobName, Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { findOrFail } from 'src/utils/misc'; @@ -72,6 +72,15 @@ export class ClusterGroupService extends BaseService { await this.clusterGroupRepository.deleteRequest(id); } + async regeneratePeople(auth: AuthDto, id: string) { + await this.requireAccess({ auth, permission: Permission.ClusterGroupRead, ids: [id] }); + + await this.jobRepository.queue({ + name: JobName.FacialRecognitionQueueAll, + data: { clusterGroupId: id, force: true }, + }); + } + async leave(auth: AuthDto, clusterGroupId: string): Promise { await this.requireAccess({ auth, permission: Permission.ClusterGroupLeave, ids: [clusterGroupId] }); diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 2f56f04548..53deeca61e 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -787,7 +787,7 @@ describe(PersonService.name, () => { await sut.handleQueueRecognizeFaces({ force: true }); - expect(mocks.person.getAllFaces).toHaveBeenCalledWith(undefined); + expect(mocks.person.getAllFaces).toHaveBeenCalledWith({}); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognition, diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 54c84eb22e..f4740a42bc 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -414,7 +414,11 @@ export class PersonService extends BaseService { } @OnJob({ name: JobName.FacialRecognitionQueueAll, queue: QueueName.FacialRecognition }) - async handleQueueRecognizeFaces({ force, nightly }: JobOf): Promise { + async handleQueueRecognizeFaces({ + force, + nightly, + clusterGroupId, + }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: false }); if (!isFacialRecognitionEnabled(machineLearning)) { return JobStatus.Skipped; @@ -437,7 +441,8 @@ export class PersonService extends BaseService { const { waiting } = await this.jobRepository.getJobCounts(QueueName.FacialRecognition); if (force) { - await this.personRepository.unassignFaces({ sourceType: SourceType.MachineLearning }); + console.log('unassigning faces'); + await this.personRepository.unassignFaces({ clusterGroupId, sourceType: SourceType.MachineLearning }); await this.handlePersonCleanup(); await this.personRepository.vacuum({ reindexVectors: false }); } else if (waiting) { @@ -452,7 +457,9 @@ export class PersonService extends BaseService { const lastRun = new Date().toISOString(); const faces = this.personRepository.getAllFaces( - force ? undefined : { personGroupId: null, sourceType: SourceType.MachineLearning }, + force + ? { clusterGroupId, sourceType: clusterGroupId ? SourceType.MachineLearning : undefined } + : { personGroupId: null, clusterGroupId, sourceType: SourceType.MachineLearning }, ); for await (const batch of batched(faces)) { await this.jobRepository.queueAll( diff --git a/server/src/types.ts b/server/src/types.ts index a7c0dbd1b7..633ed9b0c9 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -321,6 +321,10 @@ export interface IIntegrityPathWithChecksumJob { items: { path: string; reportId: string | null; checksum?: string | null }[]; } +export interface IFacialRecognitionQueueAll extends INightlyJob { + clusterGroupId?: string; +} + export interface JobCounts { active: number; completed: number; @@ -374,7 +378,7 @@ export type JobItem = // Facial Recognition | { name: JobName.AssetDetectFacesQueueAll; data: IBaseJob } | { name: JobName.AssetDetectFaces; data: IEntityJob } - | { name: JobName.FacialRecognitionQueueAll; data: INightlyJob } + | { name: JobName.FacialRecognitionQueueAll; data: IFacialRecognitionQueueAll } | { name: JobName.FacialRecognition; data: IDeferrableJob } | { name: JobName.PersonGenerateThumbnail; data: IPersonJob } diff --git a/server/test/medium/specs/services/person.service.spec.ts b/server/test/medium/specs/services/person.service.spec.ts index 579012d04c..860406fa72 100644 --- a/server/test/medium/specs/services/person.service.spec.ts +++ b/server/test/medium/specs/services/person.service.spec.ts @@ -2,14 +2,17 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto'; import { AssetFaceCreateDto } from 'src/dtos/person.dto'; +import { JobName } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { PersonRepository } from 'src/repositories/person.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { DB } from 'src/schema'; import { PersonService } from 'src/services/person.service'; import { newMediumService } from 'test/medium.factory'; @@ -21,7 +24,15 @@ let defaultDatabase: Kysely; const setup = (db?: Kysely) => { return newMediumService(PersonService, { database: db || defaultDatabase, - real: [AccessRepository, DatabaseRepository, PersonRepository, AssetRepository, AssetEditRepository], + real: [ + AccessRepository, + ConfigRepository, + DatabaseRepository, + PersonRepository, + AssetRepository, + AssetEditRepository, + SystemMetadataRepository, + ], mock: [JobRepository, LoggingRepository, StorageRepository], }); }; @@ -88,6 +99,73 @@ describe(PersonService.name, () => { }); }); + describe('handleQueueRecognizeFaces', () => { + it('should delete all people and queue faces for recognition', async () => { + const { sut, ctx } = setup(); + const jobRepo = ctx.getMock(JobRepository); + ctx.getMock(StorageRepository).unlink.mockResolvedValue(); + jobRepo.waitForQueueCompletion.mockResolvedValue(); + jobRepo.getJobCounts.mockResolvedValue({ active: 0, waiting: 0, completed: 0, delayed: 0, failed: 0, paused: 0 }); + jobRepo.queueAll.mockResolvedValue(); + + const { user } = await ctx.newUser(); + const { user: user1 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const { asset: assetUser1 } = await ctx.newAsset({ ownerId: user1.id }); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { person: personUser1 } = await ctx.newPerson({ ownerId: user1.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId }); + const { assetFace: assetFaceUser1 } = await ctx.newAssetFace({ + assetId: assetUser1.id, + personGroupId: personUser1.personGroupId, + }); + + await sut.handleQueueRecognizeFaces({ force: true }); + + await expect(ctx.database.selectFrom('person').selectAll().execute()).resolves.toHaveLength(0); + expect(jobRepo.queueAll).toHaveBeenCalledWith( + expect.objectContaining([ + { name: JobName.FacialRecognition, data: { id: assetFace.id, deferred: false } }, + { name: JobName.FacialRecognition, data: { id: assetFaceUser1.id, deferred: false } }, + ]), + ); + }); + + it('should only delete all people of a specified cluster group and queue their faces for recognition', async () => { + const { sut, ctx } = setup(); + const jobRepo = ctx.getMock(JobRepository); + ctx.getMock(StorageRepository).unlink.mockResolvedValue(); + jobRepo.waitForQueueCompletion.mockResolvedValue(); + jobRepo.getJobCounts.mockResolvedValue({ active: 0, waiting: 0, completed: 0, delayed: 0, failed: 0, paused: 0 }); + jobRepo.queueAll.mockResolvedValue(); + + const { user } = await ctx.newUser(); + const { user: user1 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const { asset: assetUser1 } = await ctx.newAsset({ ownerId: user1.id }); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { person: personUser1 } = await ctx.newPerson({ ownerId: user1.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId }); + const { assetFace: assetFaceUser1 } = await ctx.newAssetFace({ + assetId: assetUser1.id, + personGroupId: personUser1.personGroupId, + }); + + await sut.handleQueueRecognizeFaces({ force: true, clusterGroupId: user.clusterGroupId }); + + await expect(ctx.database.selectFrom('person').selectAll().execute()).resolves.toHaveLength(1); + expect(jobRepo.queueAll).toHaveBeenCalledWith( + expect.objectContaining([{ name: JobName.FacialRecognition, data: { id: assetFace.id, deferred: false } }]), + ); + expect(jobRepo.queueAll).not.toHaveBeenCalledWith( + expect.objectContaining([ + { name: JobName.FacialRecognition, data: { id: assetFace.id, deferred: false } }, + { name: JobName.FacialRecognition, data: { id: assetFaceUser1.id, deferred: false } }, + ]), + ); + }); + }); + describe('mergePerson', () => { it('should merge people of multiple users', async () => { const { sut, ctx } = setup(); diff --git a/web/src/routes/(user)/user-settings/SharingSettings.svelte b/web/src/routes/(user)/user-settings/SharingSettings.svelte index 8e0ea59938..2929c1daf8 100644 --- a/web/src/routes/(user)/user-settings/SharingSettings.svelte +++ b/web/src/routes/(user)/user-settings/SharingSettings.svelte @@ -8,6 +8,7 @@ import { handleError } from '$lib/utils/handle-error'; import { acceptClusterGroupRequest, + clusterGroupRegeneratePeople, createClusterGroupRequest, createPartner, deleteClusterGroupRequest, @@ -25,7 +26,7 @@ type PartnerResponseDto, type UserResponseDto, } from '@immich/sdk'; - import { Button, Card, CardBody, Icon, IconButton, modalManager, Text } from '@immich/ui'; + import { Button, Card, CardBody, HStack, Icon, IconButton, modalManager, Text } from '@immich/ui'; import { mdiCheck, mdiClose } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -222,6 +223,24 @@ handleError(error, $t('errors.unable_to_update_timeline_display_status')); } }; + + const handleRerunFacialRecognition = async () => { + const confirmed = await modalManager.showDialog({ + title: $t('cluster_group_facial_recognition'), + prompt: $t('cluster_group_facial_recognition_prompt'), + size: 'medium', + }); + + if (!confirmed) { + return; + } + + try { + await clusterGroupRegeneratePeople({ id: clusterGroupId }); + } catch (error) { + handleError(error, $t('errors.something_went_wrong')); + } + };
@@ -295,9 +314,12 @@ {/if} -
+ + -
+
From 2ba343e138b1851d68c80ab9867a3feb009b77b2 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 27 Aug 2026 16:39:55 +0200 Subject: [PATCH 17/42] fix: only run backport for labelled pull requests (#31052) --- .github/workflows/backport.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index e14f506c46..d4a4001452 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -9,7 +9,9 @@ permissions: {} jobs: backport: name: Backport pull request - if: ${{ github.event.pull_request.merged }} + if: >- + ${{ github.event.pull_request.merged + && contains(join(github.event.pull_request.labels.*.name, ' '), 'backport:release/') }} runs-on: ubuntu-latest permissions: {} steps: From f16fdafefc5f7e9398297822e7612b4108244779 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Thu, 27 Aug 2026 20:24:44 +0530 Subject: [PATCH 18/42] fix(web): preserve slideshow pause state when viewing video slides (#30278) Co-authored-by: priyanshuANDcoad --- .../asset-viewer/AssetViewer.svelte | 4 +++ .../asset-viewer/SlideshowBar.svelte | 26 +++++++++++-------- web/src/lib/stores/slideshow.store.ts | 1 + 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/web/src/lib/components/asset-viewer/AssetViewer.svelte b/web/src/lib/components/asset-viewer/AssetViewer.svelte index cd743d0ed1..f339442ec2 100644 --- a/web/src/lib/components/asset-viewer/AssetViewer.svelte +++ b/web/src/lib/components/asset-viewer/AssetViewer.svelte @@ -99,6 +99,7 @@ slideshowNavigation, slideshowState, slideshowRepeat, + slideshowAutoplay, } = slideshowStore; const stackThumbnailSize = 60; const stackSelectedThumbnailSize = 65; @@ -296,6 +297,9 @@ const handlePlaySlideshow = async () => { slideshowStartAssetId = asset.id; + if (!$slideshowAutoplay) { + $slideshowState = SlideshowState.PauseSlideshow; + } try { await assetViewerHtmlElement?.requestFullscreen?.(); } catch (error) { diff --git a/web/src/lib/components/asset-viewer/SlideshowBar.svelte b/web/src/lib/components/asset-viewer/SlideshowBar.svelte index 0ec5fe7dc0..26ea9fa369 100644 --- a/web/src/lib/components/asset-viewer/SlideshowBar.svelte +++ b/web/src/lib/components/asset-viewer/SlideshowBar.svelte @@ -4,7 +4,7 @@ import { ProgressBarStatus } from '$lib/constants'; import { languageManager } from '$lib/managers/language-manager.svelte'; import SlideshowSettingsModal from '$lib/modals/SlideshowSettingsModal.svelte'; - import { SlideshowNavigation, slideshowStore } from '$lib/stores/slideshow.store'; + import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { AssetTypeEnum } from '@immich/sdk'; import { IconButton, modalManager } from '@immich/ui'; import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js'; @@ -31,7 +31,7 @@ onSetToFullScreen = () => {}, }: Props = $props(); - const { restartProgress, stopProgress, slideshowDelay, showProgressBar, slideshowNavigation, slideshowAutoplay } = + const { restartProgress, stopProgress, slideshowDelay, showProgressBar, slideshowNavigation, slideshowState } = slideshowStore; let progressBarStatus: ProgressBarStatus | undefined = $state(); @@ -143,6 +143,16 @@ true, ); + const togglePause = () => { + if (progressBarStatus === ProgressBarStatus.Paused) { + $slideshowState = SlideshowState.PlaySlideshow; + progressBar?.play(); + } else { + $slideshowState = SlideshowState.PauseSlideshow; + progressBar?.pause(); + } + }; + const shortcutBindings = $derived.by((): ShortcutOptions[] => { const bindings: ShortcutOptions[] = [ { shortcut: { key: 'Escape' }, onShortcut: onClose }, @@ -154,13 +164,7 @@ if (!isVideoSlide) { bindings.push({ shortcut: { key: ' ' }, - onShortcut: () => { - if (progressBarStatus === ProgressBarStatus.Paused) { - progressBar?.play(); - } else { - progressBar?.pause(); - } - }, + onShortcut: togglePause, preventDefault: true, }); } @@ -197,7 +201,7 @@ shape="round" color="secondary" icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause} - onclick={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar?.play() : progressBar?.pause())} + onclick={togglePause} aria-label={progressBarStatus === ProgressBarStatus.Paused ? $t('play') : $t('pause')} /> {/if} @@ -240,7 +244,7 @@ {#if !isVideoSlide}