mirror of
https://github.com/immich-app/immich.git
synced 2025-12-12 15:50:43 -08:00
Compare commits
12 Commits
docs-datab
...
v1.143.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cee6bcc5ef | ||
|
|
b2f3bf7079 | ||
|
|
fe416b121c | ||
|
|
35b62cd016 | ||
|
|
b33e8abcdd | ||
|
|
0be71c82b3 | ||
|
|
a582d3a03e | ||
|
|
6609e70fa8 | ||
|
|
7a0107fc79 | ||
|
|
0bbeb20595 | ||
|
|
afc4085b55 | ||
|
|
02569d52f0 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.90",
|
"version": "2.2.91",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so
|
|||||||
|
|
||||||
### Automatic Database Dumps
|
### Automatic Database Dumps
|
||||||
|
|
||||||
:::info
|
:::warning
|
||||||
The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files.
|
The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files.
|
||||||
If the server fails to generate the database dump file, a notification will be shown in the in-app notification on the web
|
There is no monitoring for these dumps and you will not be notified if they are unsuccessful.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::caution
|
:::caution
|
||||||
|
|||||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.143.0",
|
||||||
|
"url": "https://v1.143.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.142.1",
|
"label": "v1.142.1",
|
||||||
"url": "https://v1.142.1.archive.immich.app"
|
"url": "https://v1.142.1.archive.immich.app"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.142.1",
|
"version": "1.143.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -920,7 +920,6 @@
|
|||||||
"cant_get_number_of_comments": "Can't get number of comments",
|
"cant_get_number_of_comments": "Can't get number of comments",
|
||||||
"cant_search_people": "Can't search people",
|
"cant_search_people": "Can't search people",
|
||||||
"cant_search_places": "Can't search places",
|
"cant_search_places": "Can't search places",
|
||||||
"clipboard_unsupported_mime_type": "The system clipboard does not support copying this type of content: {mimeType}",
|
|
||||||
"error_adding_assets_to_album": "Error adding assets to album",
|
"error_adding_assets_to_album": "Error adding assets to album",
|
||||||
"error_adding_users_to_album": "Error adding users to album",
|
"error_adding_users_to_album": "Error adding users to album",
|
||||||
"error_deleting_shared_user": "Error deleting shared user",
|
"error_deleting_shared_user": "Error deleting shared user",
|
||||||
@@ -1528,6 +1527,7 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||||
"preferences_settings_title": "Preferences",
|
"preferences_settings_title": "Preferences",
|
||||||
|
"preparing": "Preparing",
|
||||||
"preset": "Preset",
|
"preset": "Preset",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
@@ -1593,6 +1593,7 @@
|
|||||||
"read_changelog": "Read Changelog",
|
"read_changelog": "Read Changelog",
|
||||||
"readonly_mode_disabled": "Read-only mode disabled",
|
"readonly_mode_disabled": "Read-only mode disabled",
|
||||||
"readonly_mode_enabled": "Read-only mode enabled",
|
"readonly_mode_enabled": "Read-only mode enabled",
|
||||||
|
"ready_for_upload": "Ready for upload",
|
||||||
"reassign": "Reassign",
|
"reassign": "Reassign",
|
||||||
"reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
|
"reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
|
||||||
"reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person",
|
"reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person",
|
||||||
|
|||||||
138
machine-learning/uv.lock
generated
138
machine-learning/uv.lock
generated
@@ -507,61 +507,87 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "7.6.4"
|
version = "7.10.6"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716, upload-time = "2024-10-20T22:57:39.682Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/93/4ad92f71e28ece5c0326e5f4a6630aa4928a8846654a65cfff69b49b95b9/coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", size = 206713, upload-time = "2024-10-20T22:56:03.877Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/ae/747a580b1eda3f2e431d87de48f0604bd7bc92e52a1a95185a4aa585bc47/coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", size = 207149, upload-time = "2024-10-20T22:56:06.511Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/1a/1f573f8a6145f6d4c9130bbc120e0024daf1b24cf2a78d7393fa6eb6aba7/coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", size = 235584, upload-time = "2024-10-20T22:56:07.678Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/42/c8523f2e4db34aa9389caee0d3688b6ada7a84fcc782e943a868a7f302bd/coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", size = 233486, upload-time = "2024-10-20T22:56:09.496Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/95/565c310fffa16ede1a042e9ea1ca3962af0d8eb5543bc72df6b91dc0c3d5/coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", size = 234649, upload-time = "2024-10-20T22:56:11.326Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/81/3b550674d98968ec29c92e3e8650682be6c8b1fa7581a059e7e12e74c431/coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", size = 233744, upload-time = "2024-10-20T22:56:12.481Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/70/d66c7f51b3e33aabc5ea9f9624c1c9d9655472962270eb5e7b0d32707224/coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", size = 232204, upload-time = "2024-10-20T22:56:14.236Z" },
|
{ url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/2d/2b3a2dbed7a5f40693404c8a09e779d7c1a5fbed089d3e7224c002129ec8/coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", size = 233335, upload-time = "2024-10-20T22:56:15.521Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/4f/92d1d2ad720d698a4e71c176eacf531bfb8e0721d5ad560556f2c484a513/coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", size = 209435, upload-time = "2024-10-20T22:56:17.309Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521, upload-time = "2025-08-29T15:33:10.599Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/b9/cdf158e7991e2287bcf9082670928badb73d310047facac203ff8dcd5ff3/coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", size = 210243, upload-time = "2024-10-20T22:56:18.366Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417, upload-time = "2025-08-29T15:33:11.907Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819, upload-time = "2024-10-20T22:56:20.132Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263, upload-time = "2024-10-20T22:56:21.88Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205, upload-time = "2024-10-20T22:56:23.03Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612, upload-time = "2024-10-20T22:56:24.882Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479, upload-time = "2024-10-20T22:56:26.749Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405, upload-time = "2024-10-20T22:56:27.958Z" },
|
{ url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038, upload-time = "2024-10-20T22:56:29.816Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812, upload-time = "2024-10-20T22:56:31.654Z" },
|
{ url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400, upload-time = "2024-10-20T22:56:33.569Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562, upload-time = "2025-08-29T15:33:24.717Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243, upload-time = "2024-10-20T22:56:34.863Z" },
|
{ url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453, upload-time = "2025-08-29T15:33:26.482Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013, upload-time = "2024-10-20T22:56:36.034Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127, upload-time = "2025-08-29T15:33:27.777Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251, upload-time = "2024-10-20T22:56:38.054Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268, upload-time = "2024-10-20T22:56:40.051Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298, upload-time = "2024-10-20T22:56:41.929Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367, upload-time = "2024-10-20T22:56:43.141Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853, upload-time = "2024-10-20T22:56:44.33Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160, upload-time = "2024-10-20T22:56:46.258Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824, upload-time = "2024-10-20T22:56:48.666Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639, upload-time = "2024-10-20T22:56:50.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428, upload-time = "2024-10-20T22:56:52.468Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039, upload-time = "2024-10-20T22:56:53.656Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298, upload-time = "2024-10-20T22:56:54.979Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813, upload-time = "2024-10-20T22:56:56.209Z" },
|
{ url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959, upload-time = "2024-10-20T22:56:58.06Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950, upload-time = "2024-10-20T22:56:59.329Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610, upload-time = "2024-10-20T22:57:00.645Z" },
|
{ url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697, upload-time = "2024-10-20T22:57:01.944Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541, upload-time = "2024-10-20T22:57:03.848Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707, upload-time = "2024-10-20T22:57:05.123Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439, upload-time = "2024-10-20T22:57:06.35Z" },
|
{ url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784, upload-time = "2024-10-20T22:57:07.857Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058, upload-time = "2024-10-20T22:57:09.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772, upload-time = "2024-10-20T22:57:11.147Z" },
|
{ url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490, upload-time = "2024-10-20T22:57:13.02Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848, upload-time = "2024-10-20T22:57:14.927Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340, upload-time = "2024-10-20T22:57:16.246Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229, upload-time = "2024-10-20T22:57:17.546Z" },
|
{ url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510, upload-time = "2024-10-20T22:57:18.925Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353, upload-time = "2024-10-20T22:57:20.891Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502, upload-time = "2024-10-20T22:57:22.21Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/56/e1d75e8981a2a92c2a777e67c26efa96c66da59d645423146eb9ff3a851b/coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", size = 198954, upload-time = "2024-10-20T22:57:38.28Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -2236,16 +2262,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-cov"
|
name = "pytest-cov"
|
||||||
version = "6.2.1"
|
version = "7.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "coverage", extra = ["toml"] },
|
{ name = "coverage", extra = ["toml"] },
|
||||||
{ name = "pluggy" },
|
{ name = "pluggy" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
200
mise.toml
200
mise.toml
@@ -2,10 +2,9 @@
|
|||||||
node = "22.19.0"
|
node = "22.19.0"
|
||||||
flutter = "3.35.4"
|
flutter = "3.35.4"
|
||||||
pnpm = "10.15.1"
|
pnpm = "10.15.1"
|
||||||
dart = "3.8.2"
|
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
version = "1.31.4"
|
version = "1.30.0"
|
||||||
bin = "dcm"
|
bin = "dcm"
|
||||||
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
||||||
|
|
||||||
@@ -309,3 +308,200 @@ run = [
|
|||||||
"mise run web:test --run",
|
"mise run web:test --run",
|
||||||
"mise run web:lint",
|
"mise run web:lint",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# mobile
|
||||||
|
[tasks."mobile:codegen:dart"]
|
||||||
|
alias = "mobile:codegen"
|
||||||
|
description = "Execute build_runner to auto-generate dart code"
|
||||||
|
dir = "mobile"
|
||||||
|
sources = ["pubspec.yaml", "build.yaml", "lib/**/*.dart"]
|
||||||
|
outputs = { auto = true }
|
||||||
|
run = "dart run build_runner build --delete-conflicting-outputs"
|
||||||
|
|
||||||
|
[tasks."mobile:codegen:pigeon"]
|
||||||
|
alias = "mobile:pigeon"
|
||||||
|
description = "Generate pigeon platform code"
|
||||||
|
dir = "mobile"
|
||||||
|
depends = [
|
||||||
|
"mobile:pigeon:native-sync",
|
||||||
|
"mobile:pigeon:thumbnail",
|
||||||
|
"mobile:pigeon:background-worker",
|
||||||
|
"mobile:pigeon:background-worker-lock",
|
||||||
|
"mobile:pigeon:connectivity",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:codegen:translation"]
|
||||||
|
alias = "mobile:translation"
|
||||||
|
description = "Generate translations from i18n JSONs"
|
||||||
|
dir = "mobile"
|
||||||
|
run = [
|
||||||
|
{ task = "i18n:format-fix" },
|
||||||
|
{ tasks = [
|
||||||
|
"mobile:i18n:loader",
|
||||||
|
"mobile:i18n:keys",
|
||||||
|
] },
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:codegen:app-icon"]
|
||||||
|
description = "Generate app icons"
|
||||||
|
dir = "mobile"
|
||||||
|
run = "flutter pub run flutter_launcher_icons:main"
|
||||||
|
|
||||||
|
[tasks."mobile:codegen:splash"]
|
||||||
|
description = "Generate splash screen"
|
||||||
|
dir = "mobile"
|
||||||
|
run = "flutter pub run flutter_native_splash:create"
|
||||||
|
|
||||||
|
[tasks."mobile:test"]
|
||||||
|
description = "Run mobile tests"
|
||||||
|
dir = "mobile"
|
||||||
|
run = "flutter test"
|
||||||
|
|
||||||
|
[tasks."mobile:lint"]
|
||||||
|
description = "Analyze Dart code"
|
||||||
|
dir = "mobile"
|
||||||
|
depends = ["mobile:analyze:dart", "mobile:analyze:dcm"]
|
||||||
|
|
||||||
|
[tasks."mobile:lint-fix"]
|
||||||
|
description = "Auto-fix Dart code"
|
||||||
|
dir = "mobile"
|
||||||
|
depends = ["mobile:analyze:fix:dart", "mobile:analyze:fix:dcm"]
|
||||||
|
|
||||||
|
[tasks."mobile:format"]
|
||||||
|
description = "Format Dart code"
|
||||||
|
dir = "mobile"
|
||||||
|
run = "dart format --set-exit-if-changed $(find lib -name '*.dart' -not \\( -name '*.g.dart' -o -name '*.drift.dart' -o -name '*.gr.dart' \\))"
|
||||||
|
|
||||||
|
[tasks."mobile:build:android"]
|
||||||
|
description = "Build Android release"
|
||||||
|
dir = "mobile"
|
||||||
|
run = "flutter build appbundle"
|
||||||
|
|
||||||
|
[tasks."mobile:drift:migration"]
|
||||||
|
alias = "mobile:migration"
|
||||||
|
description = "Generate database migrations"
|
||||||
|
dir = "mobile"
|
||||||
|
run = "dart run drift_dev make-migrations"
|
||||||
|
|
||||||
|
|
||||||
|
# mobile internal tasks
|
||||||
|
[tasks."mobile:pigeon:native-sync"]
|
||||||
|
description = "Generate native sync API pigeon code"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
sources = ["pigeon/native_sync_api.dart"]
|
||||||
|
outputs = [
|
||||||
|
"lib/platform/native_sync_api.g.dart",
|
||||||
|
"ios/Runner/Sync/Messages.g.swift",
|
||||||
|
"android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt",
|
||||||
|
]
|
||||||
|
run = [
|
||||||
|
"dart run pigeon --input pigeon/native_sync_api.dart",
|
||||||
|
"dart format lib/platform/native_sync_api.g.dart",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:pigeon:thumbnail"]
|
||||||
|
description = "Generate thumbnail API pigeon code"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
sources = ["pigeon/thumbnail_api.dart"]
|
||||||
|
outputs = [
|
||||||
|
"lib/platform/thumbnail_api.g.dart",
|
||||||
|
"ios/Runner/Images/Thumbnails.g.swift",
|
||||||
|
"android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt",
|
||||||
|
]
|
||||||
|
run = [
|
||||||
|
"dart run pigeon --input pigeon/thumbnail_api.dart",
|
||||||
|
"dart format lib/platform/thumbnail_api.g.dart",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:pigeon:background-worker"]
|
||||||
|
description = "Generate background worker API pigeon code"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
sources = ["pigeon/background_worker_api.dart"]
|
||||||
|
outputs = [
|
||||||
|
"lib/platform/background_worker_api.g.dart",
|
||||||
|
"ios/Runner/Background/BackgroundWorker.g.swift",
|
||||||
|
"android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt",
|
||||||
|
]
|
||||||
|
run = [
|
||||||
|
"dart run pigeon --input pigeon/background_worker_api.dart",
|
||||||
|
"dart format lib/platform/background_worker_api.g.dart",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:pigeon:background-worker-lock"]
|
||||||
|
description = "Generate background worker lock API pigeon code"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
sources = ["pigeon/background_worker_lock_api.dart"]
|
||||||
|
outputs = [
|
||||||
|
"lib/platform/background_worker_lock_api.g.dart",
|
||||||
|
"android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt",
|
||||||
|
]
|
||||||
|
run = [
|
||||||
|
"dart run pigeon --input pigeon/background_worker_lock_api.dart",
|
||||||
|
"dart format lib/platform/background_worker_lock_api.g.dart",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:pigeon:connectivity"]
|
||||||
|
description = "Generate connectivity API pigeon code"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
sources = ["pigeon/connectivity_api.dart"]
|
||||||
|
outputs = [
|
||||||
|
"lib/platform/connectivity_api.g.dart",
|
||||||
|
"ios/Runner/Connectivity/Connectivity.g.swift",
|
||||||
|
"android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt",
|
||||||
|
]
|
||||||
|
run = [
|
||||||
|
"dart run pigeon --input pigeon/connectivity_api.dart",
|
||||||
|
"dart format lib/platform/connectivity_api.g.dart",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:i18n:loader"]
|
||||||
|
description = "Generate i18n loader"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
sources = ["i18n/"]
|
||||||
|
outputs = "lib/generated/codegen_loader.g.dart"
|
||||||
|
run = [
|
||||||
|
"dart run easy_localization:generate -S ../i18n",
|
||||||
|
"dart format lib/generated/codegen_loader.g.dart",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:i18n:keys"]
|
||||||
|
description = "Generate i18n keys"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
sources = ["i18n/en.json"]
|
||||||
|
outputs = "lib/generated/intl_keys.g.dart"
|
||||||
|
run = [
|
||||||
|
"dart run bin/generate_keys.dart",
|
||||||
|
"dart format lib/generated/intl_keys.g.dart",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks."mobile:analyze:dart"]
|
||||||
|
description = "Run Dart analysis"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
run = "dart analyze --fatal-infos"
|
||||||
|
|
||||||
|
[tasks."mobile:analyze:dcm"]
|
||||||
|
description = "Run Dart Code Metrics"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
run = "dcm analyze lib --fatal-style --fatal-warnings"
|
||||||
|
|
||||||
|
[tasks."mobile:analyze:fix:dart"]
|
||||||
|
description = "Auto-fix Dart analysis"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
run = "dart fix --apply"
|
||||||
|
|
||||||
|
[tasks."mobile:analyze:fix:dcm"]
|
||||||
|
description = "Auto-fix Dart Code Metrics"
|
||||||
|
dir = "mobile"
|
||||||
|
hide = true
|
||||||
|
run = "dcm fix lib"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.os.ext.SdkExtensions
|
|||||||
import app.alextran.immich.background.BackgroundEngineLock
|
import app.alextran.immich.background.BackgroundEngineLock
|
||||||
import app.alextran.immich.background.BackgroundWorkerApiImpl
|
import app.alextran.immich.background.BackgroundWorkerApiImpl
|
||||||
import app.alextran.immich.background.BackgroundWorkerFgHostApi
|
import app.alextran.immich.background.BackgroundWorkerFgHostApi
|
||||||
|
import app.alextran.immich.background.BackgroundWorkerLockApi
|
||||||
import app.alextran.immich.connectivity.ConnectivityApi
|
import app.alextran.immich.connectivity.ConnectivityApi
|
||||||
import app.alextran.immich.connectivity.ConnectivityApiImpl
|
import app.alextran.immich.connectivity.ConnectivityApiImpl
|
||||||
import app.alextran.immich.images.ThumbnailApi
|
import app.alextran.immich.images.ThumbnailApi
|
||||||
@@ -24,11 +25,9 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
||||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
|
||||||
flutterEngine.plugins.add(HttpSSLOptionsPlugin())
|
|
||||||
flutterEngine.plugins.add(BackgroundEngineLock())
|
|
||||||
|
|
||||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||||
|
val backgroundEngineLockImpl = BackgroundEngineLock(ctx)
|
||||||
|
BackgroundWorkerLockApi.setUp(messenger, backgroundEngineLockImpl)
|
||||||
val nativeSyncApiImpl =
|
val nativeSyncApiImpl =
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) {
|
||||||
NativeSyncApiImpl26(ctx)
|
NativeSyncApiImpl26(ctx)
|
||||||
@@ -39,6 +38,10 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx))
|
ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx))
|
||||||
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
||||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||||
|
|
||||||
|
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||||
|
flutterEngine.plugins.add(HttpSSLOptionsPlugin())
|
||||||
|
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,50 @@
|
|||||||
package app.alextran.immich.background
|
package app.alextran.immich.background
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.WorkManager
|
|
||||||
import io.flutter.embedding.engine.FlutterEngineCache
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
private const val TAG = "BackgroundEngineLock"
|
private const val TAG = "BackgroundEngineLock"
|
||||||
|
|
||||||
class BackgroundEngineLock : FlutterPlugin {
|
class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, FlutterPlugin {
|
||||||
companion object {
|
private val ctx: Context = context.applicationContext
|
||||||
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
|
|
||||||
var engineCount = AtomicInteger(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
companion object {
|
||||||
// work manager task is running while the main app is opened, cancel the worker
|
|
||||||
if (engineCount.incrementAndGet() > 1 && FlutterEngineCache.getInstance()
|
private var engineCount = AtomicInteger(0)
|
||||||
.get(ENGINE_CACHE_KEY) != null
|
|
||||||
) {
|
private fun checkAndEnforceBackgroundLock(ctx: Context) {
|
||||||
WorkManager.getInstance(binding.applicationContext)
|
// work manager task is running while the main app is opened, cancel the worker
|
||||||
.cancelUniqueWork(BackgroundWorkerApiImpl.BACKGROUND_WORKER_NAME)
|
if (BackgroundWorkerPreferences(ctx).isLocked() &&
|
||||||
FlutterEngineCache.getInstance().remove(ENGINE_CACHE_KEY)
|
engineCount.get() > 1 &&
|
||||||
|
BackgroundWorkerApiImpl.isBackgroundWorkerRunning()
|
||||||
|
) {
|
||||||
|
Log.i(TAG, "Background worker is locked, cancelling the background worker")
|
||||||
|
BackgroundWorkerApiImpl.cancelBackgroundWorker(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun lock() {
|
||||||
engineCount.decrementAndGet()
|
BackgroundWorkerPreferences(ctx).setLocked(true)
|
||||||
Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount")
|
checkAndEnforceBackgroundLock(ctx)
|
||||||
}
|
Log.i(TAG, "Background worker is locked")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unlock() {
|
||||||
|
BackgroundWorkerPreferences(ctx).setLocked(false)
|
||||||
|
Log.i(TAG, "Background worker is unlocked")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
checkAndEnforceBackgroundLock(binding.applicationContext)
|
||||||
|
engineCount.incrementAndGet()
|
||||||
|
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
engineCount.decrementAndGet()
|
||||||
|
Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
|
|
||||||
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
engine = FlutterEngine(ctx)
|
engine = FlutterEngine(ctx)
|
||||||
FlutterEngineCache.getInstance().remove(BackgroundEngineLock.ENGINE_CACHE_KEY);
|
FlutterEngineCache.getInstance().put(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY, engine!!)
|
||||||
FlutterEngineCache.getInstance()
|
|
||||||
.put(BackgroundEngineLock.ENGINE_CACHE_KEY, engine!!)
|
|
||||||
|
|
||||||
// Register custom plugins
|
// Register custom plugins
|
||||||
MainActivity.registerPlugins(ctx, engine!!)
|
MainActivity.registerPlugins(ctx, engine!!)
|
||||||
@@ -192,9 +190,9 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
isComplete = true
|
isComplete = true
|
||||||
engine?.destroy()
|
engine?.destroy()
|
||||||
engine = null
|
engine = null
|
||||||
FlutterEngineCache.getInstance().remove(BackgroundEngineLock.ENGINE_CACHE_KEY);
|
|
||||||
flutterApi = null
|
flutterApi = null
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
FlutterEngineCache.getInstance().remove(BackgroundWorkerApiImpl.ENGINE_CACHE_KEY)
|
||||||
waitForForegroundPromotion()
|
waitForForegroundPromotion()
|
||||||
completionHandler.set(success)
|
completionHandler.set(success)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.alextran.immich.background
|
package app.alextran.immich.background
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
@@ -9,6 +8,7 @@ import androidx.work.Constraints
|
|||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import io.flutter.embedding.engine.FlutterEngineCache
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private const val TAG = "BackgroundWorkerApiImpl"
|
private const val TAG = "BackgroundWorkerApiImpl"
|
||||||
@@ -34,8 +34,10 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
||||||
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
||||||
|
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
|
||||||
|
|
||||||
|
|
||||||
fun enqueueMediaObserver(ctx: Context) {
|
fun enqueueMediaObserver(ctx: Context) {
|
||||||
val settings = BackgroundWorkerPreferences(ctx).getSettings()
|
val settings = BackgroundWorkerPreferences(ctx).getSettings()
|
||||||
@@ -73,35 +75,18 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
|
|
||||||
Log.i(TAG, "Enqueued background worker with name: $BACKGROUND_WORKER_NAME")
|
Log.i(TAG, "Enqueued background worker with name: $BACKGROUND_WORKER_NAME")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BackgroundWorkerPreferences(private val ctx: Context) {
|
fun isBackgroundWorkerRunning(): Boolean {
|
||||||
companion object {
|
// Easier to check if the engine is cached as we always cache the engine when starting the worker
|
||||||
private const val SHARED_PREF_NAME = "Immich::BackgroundWorker"
|
// and remove it when the worker is finished
|
||||||
private const val SHARED_PREF_MIN_DELAY_KEY = "BackgroundWorker::minDelaySeconds"
|
return FlutterEngineCache.getInstance().get(ENGINE_CACHE_KEY) != null
|
||||||
private const val SHARED_PREF_REQUIRE_CHARGING_KEY = "BackgroundWorker::requireCharging"
|
}
|
||||||
|
|
||||||
private const val DEFAULT_MIN_DELAY_SECONDS = 30L
|
fun cancelBackgroundWorker(ctx: Context) {
|
||||||
private const val DEFAULT_REQUIRE_CHARGING = false
|
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||||
}
|
FlutterEngineCache.getInstance().remove(ENGINE_CACHE_KEY)
|
||||||
|
|
||||||
private val sp: SharedPreferences by lazy {
|
Log.i(TAG, "Cancelled background upload task")
|
||||||
ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSettings(settings: BackgroundWorkerSettings) {
|
|
||||||
sp.edit().apply {
|
|
||||||
putLong(SHARED_PREF_MIN_DELAY_KEY, settings.minimumDelaySeconds)
|
|
||||||
putBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, settings.requiresCharging)
|
|
||||||
apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSettings(): BackgroundWorkerSettings {
|
|
||||||
return BackgroundWorkerSettings(
|
|
||||||
minimumDelaySeconds = sp.getLong(SHARED_PREF_MIN_DELAY_KEY, DEFAULT_MIN_DELAY_SECONDS),
|
|
||||||
requiresCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, DEFAULT_REQUIRE_CHARGING),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||||
|
|
||||||
|
package app.alextran.immich.background
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMethodCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
private object BackgroundWorkerLockPigeonUtils {
|
||||||
|
|
||||||
|
fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
return if (exception is FlutterError) {
|
||||||
|
listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private open class BackgroundWorkerLockPigeonCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface BackgroundWorkerLockApi {
|
||||||
|
fun lock()
|
||||||
|
fun unlock()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by BackgroundWorkerLockApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
BackgroundWorkerLockPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `BackgroundWorkerLockApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerLockApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.lock()
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
BackgroundWorkerLockPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.unlock()
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
BackgroundWorkerLockPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package app.alextran.immich.background
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
|
class BackgroundWorkerPreferences(private val ctx: Context) {
|
||||||
|
companion object {
|
||||||
|
const val SHARED_PREF_NAME = "Immich::BackgroundWorker"
|
||||||
|
private const val SHARED_PREF_MIN_DELAY_KEY = "BackgroundWorker::minDelaySeconds"
|
||||||
|
private const val SHARED_PREF_REQUIRE_CHARGING_KEY = "BackgroundWorker::requireCharging"
|
||||||
|
private const val SHARED_PREF_LOCK_KEY = "BackgroundWorker::isLocked"
|
||||||
|
|
||||||
|
private const val DEFAULT_MIN_DELAY_SECONDS = 30L
|
||||||
|
private const val DEFAULT_REQUIRE_CHARGING = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sp: SharedPreferences by lazy {
|
||||||
|
ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSettings(settings: BackgroundWorkerSettings) {
|
||||||
|
sp.edit {
|
||||||
|
putLong(SHARED_PREF_MIN_DELAY_KEY, settings.minimumDelaySeconds)
|
||||||
|
putBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, settings.requiresCharging)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSettings(): BackgroundWorkerSettings {
|
||||||
|
return BackgroundWorkerSettings(
|
||||||
|
minimumDelaySeconds = sp.getLong(SHARED_PREF_MIN_DELAY_KEY, DEFAULT_MIN_DELAY_SECONDS),
|
||||||
|
requiresCharging = sp.getBoolean(
|
||||||
|
SHARED_PREF_REQUIRE_CHARGING_KEY,
|
||||||
|
DEFAULT_REQUIRE_CHARGING
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocked(paused: Boolean) {
|
||||||
|
sp.edit {
|
||||||
|
putBoolean(SHARED_PREF_LOCK_KEY, paused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLocked(): Boolean {
|
||||||
|
return sp.getBoolean(SHARED_PREF_LOCK_KEY, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ import java.util.concurrent.Executors
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
|
import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.concurrent.CancellationException
|
import java.util.concurrent.CancellationException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@@ -120,15 +121,14 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
|
|||||||
signal: CancellationSignal
|
signal: CancellationSignal
|
||||||
) {
|
) {
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val targetWidth = width.toInt()
|
val size = Size(width.toInt(), height.toInt())
|
||||||
val targetHeight = height.toInt()
|
|
||||||
val id = assetId.toLong()
|
val id = assetId.toLong()
|
||||||
|
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val bitmap = if (isVideo) {
|
val bitmap = if (isVideo) {
|
||||||
decodeVideoThumbnail(id, targetWidth, targetHeight, signal)
|
decodeVideoThumbnail(id, size, signal)
|
||||||
} else {
|
} else {
|
||||||
decodeImage(id, targetWidth, targetHeight, signal)
|
decodeImage(id, size, signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
processBitmap(bitmap, callback, signal)
|
processBitmap(bitmap, callback, signal)
|
||||||
@@ -151,9 +151,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
|
|||||||
bitmap.recycle()
|
bitmap.recycle()
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val res = mapOf(
|
val res = mapOf(
|
||||||
"pointer" to pointer,
|
"pointer" to pointer, "width" to actualWidth.toLong(), "height" to actualHeight.toLong()
|
||||||
"width" to actualWidth.toLong(),
|
|
||||||
"height" to actualHeight.toLong()
|
|
||||||
)
|
)
|
||||||
callback(Result.success(res))
|
callback(Result.success(res))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -162,55 +160,54 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeImage(
|
private fun decodeImage(id: Long, size: Size, signal: CancellationSignal): Bitmap {
|
||||||
id: Long, targetWidth: Int, targetHeight: Int, signal: CancellationSignal
|
|
||||||
): Bitmap {
|
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id)
|
val uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||||
if (targetHeight > 768 || targetWidth > 768) {
|
if (size.width <= 0 || size.height <= 0 || size.width > 768 || size.height > 768) {
|
||||||
return decodeSource(uri, targetWidth, targetHeight, signal)
|
return decodeSource(uri, size, signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
|
resolver.loadThumbnail(uri, size, signal)
|
||||||
} else {
|
} else {
|
||||||
signal.setOnCancelListener { Images.Thumbnails.cancelThumbnailRequest(resolver, id) }
|
signal.setOnCancelListener { Images.Thumbnails.cancelThumbnailRequest(resolver, id) }
|
||||||
Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, OPTIONS)
|
Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, OPTIONS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeVideoThumbnail(
|
private fun decodeVideoThumbnail(id: Long, target: Size, signal: CancellationSignal): Bitmap {
|
||||||
id: Long, targetWidth: Int, targetHeight: Int, signal: CancellationSignal
|
|
||||||
): Bitmap {
|
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id)
|
val uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id)
|
||||||
resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
|
// ensure a valid resolution as the thumbnail is used for videos even when no scaling is needed
|
||||||
|
val size = if (target.width > 0 && target.height > 0) target else Size(768, 768)
|
||||||
|
resolver.loadThumbnail(uri, size, signal)
|
||||||
} else {
|
} else {
|
||||||
signal.setOnCancelListener { Video.Thumbnails.cancelThumbnailRequest(resolver, id) }
|
signal.setOnCancelListener { Video.Thumbnails.cancelThumbnailRequest(resolver, id) }
|
||||||
Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, OPTIONS)
|
Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, OPTIONS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeSource(
|
private fun decodeSource(uri: Uri, target: Size, signal: CancellationSignal): Bitmap {
|
||||||
uri: Uri, targetWidth: Int, targetHeight: Int, signal: CancellationSignal
|
|
||||||
): Bitmap {
|
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val source = ImageDecoder.createSource(resolver, uri)
|
val source = ImageDecoder.createSource(resolver, uri)
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
|
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
|
||||||
if (targetWidth > 0 && targetHeight > 0) {
|
if (target.width > 0 && target.height > 0) {
|
||||||
val sample = max(1, min(info.size.width / targetWidth, info.size.height / targetHeight))
|
val sample = max(1, min(info.size.width / target.width, info.size.height / target.height))
|
||||||
decoder.setTargetSampleSize(sample)
|
decoder.setTargetSampleSize(sample)
|
||||||
}
|
}
|
||||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||||
decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
|
decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val ref = Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri)
|
val ref =
|
||||||
.disallowHardwareConfig().format(DecodeFormat.PREFER_ARGB_8888)
|
Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri).disallowHardwareConfig()
|
||||||
.submit(targetWidth, targetHeight)
|
.format(DecodeFormat.PREFER_ARGB_8888).submit(
|
||||||
|
if (target.width > 0) target.width else SIZE_ORIGINAL,
|
||||||
|
if (target.height > 0) target.height else SIZE_ORIGINAL,
|
||||||
|
)
|
||||||
signal.setOnCancelListener { Glide.with(ctx).clear(ref) }
|
signal.setOnCancelListener { Glide.with(ctx).clear(ref) }
|
||||||
ref.get()
|
ref.get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 3015,
|
"android.injected.version.code" => 3016,
|
||||||
"android.injected.version.name" => "1.142.1",
|
"android.injected.version.name" => "1.143.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ platform :ios do
|
|||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
)
|
)
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.142.1"
|
version_number: "1.143.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import 'package:immich_mobile/constants/constants.dart';
|
|||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/intl_keys.g.dart';
|
import 'package:immich_mobile/generated/intl_keys.g.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
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/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.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/drift_backup.provider.dart';
|
||||||
@@ -58,7 +60,7 @@ class BackgroundWorkerFgService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
late final ProviderContainer _ref;
|
ProviderContainer? _ref;
|
||||||
final Isar _isar;
|
final Isar _isar;
|
||||||
final Drift _drift;
|
final Drift _drift;
|
||||||
final DriftLogger _driftLogger;
|
final DriftLogger _driftLogger;
|
||||||
@@ -83,29 +85,31 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
BackgroundWorkerFlutterApi.setUp(this);
|
BackgroundWorkerFlutterApi.setUp(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
bool get _isBackupEnabled => _ref?.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup) ?? false;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
try {
|
try {
|
||||||
HttpSSLOptions.apply(applyNative: false);
|
HttpSSLOptions.apply(applyNative: false);
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait(
|
||||||
loadTranslations(),
|
[
|
||||||
workerManager.init(dynamicSpawning: true),
|
loadTranslations(),
|
||||||
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(),
|
workerManager.init(dynamicSpawning: true),
|
||||||
// Initialize the file downloader
|
_ref?.read(authServiceProvider).setOpenApiServiceEndpoint(),
|
||||||
FileDownloader().configure(
|
// Initialize the file downloader
|
||||||
globalConfig: [
|
FileDownloader().configure(
|
||||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
globalConfig: [
|
||||||
(Config.holdingQueue, (6, 6, 3)),
|
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||||
// On Android, if files are larger than 256MB, run in foreground service
|
(Config.holdingQueue, (6, 6, 3)),
|
||||||
(Config.runInForegroundIfFileLargerThan, 256),
|
// On Android, if files are larger than 256MB, run in foreground service
|
||||||
],
|
(Config.runInForegroundIfFileLargerThan, 256),
|
||||||
),
|
],
|
||||||
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
|
),
|
||||||
FileDownloader().trackTasks(),
|
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
|
||||||
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
|
FileDownloader().trackTasks(),
|
||||||
]);
|
_ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
|
||||||
|
].nonNulls,
|
||||||
|
);
|
||||||
|
|
||||||
configureFileDownloaderNotifications();
|
configureFileDownloaderNotifications();
|
||||||
|
|
||||||
@@ -178,15 +182,17 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cleanup() async {
|
Future<void> _cleanup() async {
|
||||||
if (_isCleanedUp) {
|
// If ref is null, it means the service was never initialized properly
|
||||||
|
if (_isCleanedUp || _ref == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final backgroundSyncManager = _ref.read(backgroundSyncProvider);
|
|
||||||
final nativeSyncApi = _ref.read(nativeSyncApiProvider);
|
|
||||||
_isCleanedUp = true;
|
_isCleanedUp = true;
|
||||||
_ref.dispose();
|
final backgroundSyncManager = _ref?.read(backgroundSyncProvider);
|
||||||
|
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
|
||||||
|
_ref?.dispose();
|
||||||
|
_ref = null;
|
||||||
|
|
||||||
_cancellationToken.cancel();
|
_cancellationToken.cancel();
|
||||||
_logger.info("Cleaning up background worker");
|
_logger.info("Cleaning up background worker");
|
||||||
@@ -199,14 +205,14 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
Store.dispose(),
|
Store.dispose(),
|
||||||
_drift.close(),
|
_drift.close(),
|
||||||
_driftLogger.close(),
|
_driftLogger.close(),
|
||||||
backgroundSyncManager.cancel(),
|
backgroundSyncManager?.cancel(),
|
||||||
nativeSyncApi.cancelHashing(),
|
nativeSyncApi?.cancelHashing(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (_isar.isOpen) {
|
if (_isar.isOpen) {
|
||||||
cleanupFutures.add(_isar.close());
|
cleanupFutures.add(_isar.close());
|
||||||
}
|
}
|
||||||
await Future.wait(cleanupFutures);
|
await Future.wait(cleanupFutures.nonNulls);
|
||||||
_logger.info("Background worker resources cleaned up");
|
_logger.info("Background worker resources cleaned up");
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
|
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
|
||||||
@@ -216,14 +222,18 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
Future<void> _handleBackup() async {
|
Future<void> _handleBackup() async {
|
||||||
await runZonedGuarded(
|
await runZonedGuarded(
|
||||||
() async {
|
() async {
|
||||||
if (!_isBackupEnabled || _isCleanedUp) {
|
if (_isCleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isBackupEnabled) {
|
||||||
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
|
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
|
||||||
|
|
||||||
final currentUser = _ref.read(currentUserProvider);
|
final currentUser = _ref?.read(currentUserProvider);
|
||||||
if (currentUser == null) {
|
if (currentUser == null) {
|
||||||
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
|
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
|
||||||
return;
|
return;
|
||||||
@@ -231,19 +241,18 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
|
|
||||||
_logger.info("[_handleBackup 4] Resume backup from background");
|
_logger.info("[_handleBackup 4] Resume backup from background");
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
return _ref?.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
final canPing = await _ref.read(serverInfoServiceProvider).ping();
|
final canPing = await _ref?.read(serverInfoServiceProvider).ping() ?? false;
|
||||||
if (!canPing) {
|
if (!canPing) {
|
||||||
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
|
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
|
final networkCapabilities = await _ref?.read(connectivityApiProvider).getCapabilities() ?? [];
|
||||||
|
|
||||||
return _ref
|
return _ref
|
||||||
.read(uploadServiceProvider)
|
?.read(uploadServiceProvider)
|
||||||
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
|
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
|
||||||
},
|
},
|
||||||
(error, stack) {
|
(error, stack) {
|
||||||
@@ -253,18 +262,18 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
||||||
await _ref.read(backgroundSyncProvider).syncLocal();
|
await _ref?.read(backgroundSyncProvider).syncLocal();
|
||||||
if (_isCleanedUp) {
|
if (_isCleanedUp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _ref.read(backgroundSyncProvider).syncRemote();
|
await _ref?.read(backgroundSyncProvider).syncRemote();
|
||||||
if (_isCleanedUp) {
|
if (_isCleanedUp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
var hashFuture = _ref?.read(backgroundSyncProvider).hashAssets();
|
||||||
if (hashTimeout != null) {
|
if (hashTimeout != null && hashFuture != null) {
|
||||||
hashFuture = hashFuture.timeout(
|
hashFuture = hashFuture.timeout(
|
||||||
hashTimeout,
|
hashTimeout,
|
||||||
onTimeout: () {
|
onTimeout: () {
|
||||||
@@ -277,6 +286,23 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BackgroundWorkerLockService {
|
||||||
|
final BackgroundWorkerLockApi _hostApi;
|
||||||
|
const BackgroundWorkerLockService(this._hostApi);
|
||||||
|
|
||||||
|
Future<void> lock() async {
|
||||||
|
if (CurrentPlatform.isAndroid) {
|
||||||
|
return _hostApi.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unlock() async {
|
||||||
|
if (CurrentPlatform.isAndroid) {
|
||||||
|
return _hostApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Native entry invoked from the background worker. If renaming or moving this to a different
|
/// Native entry invoked from the background worker. If renaming or moving this to a different
|
||||||
/// library, make sure to update the entry points and URI in native workers as well
|
/// library, make sure to update the entry points and URI in native workers as well
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
final syncLinkedAlbumServiceProvider = Provider(
|
final syncLinkedAlbumServiceProvider = Provider(
|
||||||
(ref) => SyncLinkedAlbumService(
|
(ref) => SyncLinkedAlbumService(
|
||||||
@@ -31,17 +31,19 @@ class SyncLinkedAlbumService {
|
|||||||
selectedAlbums.map((localAlbum) async {
|
selectedAlbums.map((localAlbum) async {
|
||||||
final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId;
|
final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId;
|
||||||
if (linkedRemoteAlbumId == null) {
|
if (linkedRemoteAlbumId == null) {
|
||||||
|
_log.warning("No linked remote album ID found for local album: ${localAlbum.name}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId);
|
final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId);
|
||||||
if (remoteAlbum == null) {
|
if (remoteAlbum == null) {
|
||||||
|
_log.warning("Linked remote album not found for ID: $linkedRemoteAlbumId");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get assets that are uploaded but not in the remote album
|
// get assets that are uploaded but not in the remote album
|
||||||
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
|
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
|
||||||
|
_log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}");
|
||||||
if (assetIds.isNotEmpty) {
|
if (assetIds.isNotEmpty) {
|
||||||
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
|
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
|
||||||
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||||
final user = Store.tryGet(StoreKey.currentUser);
|
final user = Store.tryGet(StoreKey.currentUser);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync");
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
|
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
|
||||||
|
|||||||
@@ -29,82 +29,56 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.excluded));
|
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.excluded));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getTotalCount() async {
|
/// Returns all backup-related counts in a single query.
|
||||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
///
|
||||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
/// - total: number of distinct assets in selected albums, excluding those that are also in excluded albums
|
||||||
..join([
|
/// - backup: number of those assets that already exist on the server for [userId]
|
||||||
innerJoin(
|
/// - remainder: number of those assets that do not yet exist on the server for [userId]
|
||||||
_db.localAlbumEntity,
|
/// (includes processing)
|
||||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
/// - processing: number of those assets that are still preparing/have a null checksum
|
||||||
useColumns: false,
|
Future<({int total, int remainder, int processing})> getAllCounts(String userId) async {
|
||||||
),
|
const sql = '''
|
||||||
])
|
SELECT
|
||||||
..where(
|
COUNT(*) AS total_count,
|
||||||
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
|
COUNT(*) FILTER (WHERE lae.checksum IS NULL) AS processing_count,
|
||||||
_db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()),
|
COUNT(*) FILTER (WHERE rae.id IS NULL) AS remainder_count
|
||||||
);
|
FROM local_asset_entity lae
|
||||||
|
LEFT JOIN main.remote_asset_entity rae
|
||||||
|
ON lae.checksum = rae.checksum AND rae.owner_id = ?1
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM local_album_asset_entity laa
|
||||||
|
INNER JOIN main.local_album_entity la on laa.album_id = la.id
|
||||||
|
WHERE laa.asset_id = lae.id
|
||||||
|
AND la.backup_selection = ?2
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM local_album_asset_entity laa
|
||||||
|
INNER JOIN main.local_album_entity la on laa.album_id = la.id
|
||||||
|
WHERE laa.asset_id = lae.id
|
||||||
|
AND la.backup_selection = ?3
|
||||||
|
);
|
||||||
|
''';
|
||||||
|
|
||||||
return query.get().then((rows) => rows.length);
|
final row = await _db
|
||||||
}
|
.customSelect(
|
||||||
|
sql,
|
||||||
|
variables: [
|
||||||
|
Variable.withString(userId),
|
||||||
|
Variable.withInt(BackupSelection.selected.index),
|
||||||
|
Variable.withInt(BackupSelection.excluded.index),
|
||||||
|
],
|
||||||
|
readsFrom: {_db.localAlbumAssetEntity, _db.localAlbumEntity, _db.localAssetEntity, _db.remoteAssetEntity},
|
||||||
|
)
|
||||||
|
.getSingle();
|
||||||
|
|
||||||
Future<int> getRemainderCount(String userId) async {
|
final data = row.data;
|
||||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
return (
|
||||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
total: (data['total_count'] as int?) ?? 0,
|
||||||
..join([
|
remainder: (data['remainder_count'] as int?) ?? 0,
|
||||||
innerJoin(
|
processing: (data['processing_count'] as int?) ?? 0,
|
||||||
_db.localAlbumEntity,
|
);
|
||||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
innerJoin(
|
|
||||||
_db.localAssetEntity,
|
|
||||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
leftOuterJoin(
|
|
||||||
_db.remoteAssetEntity,
|
|
||||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) &
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
..where(
|
|
||||||
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
|
|
||||||
_db.remoteAssetEntity.id.isNull() &
|
|
||||||
_db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()),
|
|
||||||
);
|
|
||||||
|
|
||||||
return query.get().then((rows) => rows.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> getBackupCount(String userId) async {
|
|
||||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
|
||||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
|
||||||
..join([
|
|
||||||
innerJoin(
|
|
||||||
_db.localAlbumEntity,
|
|
||||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
innerJoin(
|
|
||||||
_db.localAssetEntity,
|
|
||||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
innerJoin(
|
|
||||||
_db.remoteAssetEntity,
|
|
||||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
..where(
|
|
||||||
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
|
|
||||||
_db.remoteAssetEntity.id.isNotNull() &
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
_db.localAlbumAssetEntity.assetId.isNotInQuery(_getExcludedSubquery()),
|
|
||||||
);
|
|
||||||
|
|
||||||
return query.get().then((rows) => rows.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<LocalAsset>> getCandidates(String userId) async {
|
Future<List<LocalAsset>> getCandidates(String userId) async {
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
|
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
@@ -32,6 +34,7 @@ import 'package:immich_mobile/theme/dynamic_theme.dart';
|
|||||||
import 'package:immich_mobile/theme/theme_data.dart';
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||||
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||||
import 'package:immich_mobile/utils/licenses.dart';
|
import 'package:immich_mobile/utils/licenses.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
@@ -39,10 +42,10 @@ import 'package:intl/date_symbol_data_local.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:timezone/data/latest.dart';
|
import 'package:timezone/data/latest.dart';
|
||||||
import 'package:worker_manager/worker_manager.dart';
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
ImmichWidgetsBinding();
|
||||||
|
unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock());
|
||||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
await Bootstrap.initDomain(isar, drift, logDb);
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await initApp();
|
await initApp();
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import 'package:immich_mobile/providers/sync_status.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftBackupPage extends ConsumerStatefulWidget {
|
class DriftBackupPage extends ConsumerStatefulWidget {
|
||||||
@@ -29,6 +32,9 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
WakelockPlus.enable();
|
||||||
|
|
||||||
final currentUser = ref.read(currentUserProvider);
|
final currentUser = ref.read(currentUserProvider);
|
||||||
if (currentUser == null) {
|
if (currentUser == null) {
|
||||||
return;
|
return;
|
||||||
@@ -44,6 +50,12 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
super.dispose();
|
||||||
|
WakelockPlus.disable();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final selectedAlbum = ref
|
final selectedAlbum = ref
|
||||||
@@ -260,12 +272,205 @@ class _RemainderCard extends ConsumerWidget {
|
|||||||
final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount));
|
final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount));
|
||||||
final syncStatus = ref.watch(syncStatusProvider);
|
final syncStatus = ref.watch(syncStatusProvider);
|
||||||
|
|
||||||
return BackupInfoCard(
|
return Card(
|
||||||
title: "backup_controller_page_remainder".tr(),
|
shape: RoundedRectangleBorder(
|
||||||
subtitle: "backup_controller_page_remainder_sub".tr(),
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
info: remainderCount.toString(),
|
side: BorderSide(color: context.colorScheme.outlineVariant, width: 1),
|
||||||
isLoading: syncStatus.isRemoteSyncing,
|
),
|
||||||
onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()),
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
minVerticalPadding: 18,
|
||||||
|
isThreeLine: true,
|
||||||
|
title: Text("backup_controller_page_remainder".t(context: context), style: context.textTheme.titleMedium),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4.0, right: 18.0),
|
||||||
|
child: Text(
|
||||||
|
"backup_controller_page_remainder_sub".t(context: context),
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
remainderCount.toString(),
|
||||||
|
style: context.textTheme.titleLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(syncStatus.isRemoteSyncing ? 50 : 255),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (syncStatus.isRemoteSyncing)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"backup_info_card_assets",
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(syncStatus.isRemoteSyncing ? 50 : 255),
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
const _PreparingStatus(),
|
||||||
|
const Divider(height: 0),
|
||||||
|
|
||||||
|
ListTile(
|
||||||
|
enableFeedback: true,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 0.0),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()),
|
||||||
|
title: Text(
|
||||||
|
"view_details".t(context: context),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)),
|
||||||
|
),
|
||||||
|
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreparingStatus extends ConsumerStatefulWidget {
|
||||||
|
const _PreparingStatus();
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PreparingStatusState createState() => _PreparingStatusState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreparingStatusState extends ConsumerState {
|
||||||
|
Timer? _pollingTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pollingTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startPollingIfNeeded() {
|
||||||
|
if (_pollingTimer != null) return;
|
||||||
|
|
||||||
|
_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);
|
||||||
|
|
||||||
|
// Stop polling if processing count reaches 0
|
||||||
|
final updatedProcessingCount = ref.read(driftBackupProvider.select((p) => p.processingCount));
|
||||||
|
if (updatedProcessingCount == 0) {
|
||||||
|
timer.cancel();
|
||||||
|
_pollingTimer = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timer.cancel();
|
||||||
|
_pollingTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 readyForUploadCount = remainderCount - processingCount;
|
||||||
|
|
||||||
|
ref.listen<int>(driftBackupProvider.select((p) => p.processingCount), (previous, next) {
|
||||||
|
if (next > 0 && _pollingTimer == null) {
|
||||||
|
_startPollingIfNeeded();
|
||||||
|
} else if (next == 0 && _pollingTimer != null) {
|
||||||
|
_pollingTimer?.cancel();
|
||||||
|
_pollingTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!syncStatus.isHashing) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 1.0),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainerHigh.withValues(alpha: 0.5),
|
||||||
|
shape: BoxShape.rectangle,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"preparing".t(context: context),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 1.5)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
processingCount.toString(),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
|
decoration: BoxDecoration(color: context.colorScheme.primary.withValues(alpha: 0.1)),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"ready_for_upload".t(context: context),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
readyForUploadCount.toString(),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|||||||
97
mobile/lib/platform/background_worker_lock_api.g.dart
generated
Normal file
97
mobile/lib/platform/background_worker_lock_api.g.dart
generated
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
PlatformException _createConnectionError(String channelName) {
|
||||||
|
return PlatformException(
|
||||||
|
code: 'channel-error',
|
||||||
|
message: 'Unable to establish connection on channel: "$channelName".',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
|
const _PigeonCodec();
|
||||||
|
@override
|
||||||
|
void writeValue(WriteBuffer buffer, Object? value) {
|
||||||
|
if (value is int) {
|
||||||
|
buffer.putUint8(4);
|
||||||
|
buffer.putInt64(value);
|
||||||
|
} else {
|
||||||
|
super.writeValue(buffer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
|
switch (type) {
|
||||||
|
default:
|
||||||
|
return super.readValueOfType(type, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackgroundWorkerLockApi {
|
||||||
|
/// Constructor for [BackgroundWorkerLockApi]. The [binaryMessenger] named argument is
|
||||||
|
/// available for dependency injection. If it is left null, the default
|
||||||
|
/// BinaryMessenger will be used which routes to the host platform.
|
||||||
|
BackgroundWorkerLockApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||||
|
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||||
|
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
|
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||||
|
|
||||||
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
|
Future<void> lock() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unlock() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext buildContext) {
|
builder: (BuildContext buildContext) {
|
||||||
ref.read(actionProvider.notifier).shareAssets(source).then((ActionResult result) {
|
ref.read(actionProvider.notifier).shareAssets(source, context).then((ActionResult result) {
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
final _scrollController = ScrollController();
|
late final ScrollController _scrollController;
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
// Drag selection state
|
// Drag selection state
|
||||||
@@ -119,10 +119,12 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
int _perRow = 4;
|
int _perRow = 4;
|
||||||
double _scaleFactor = 3.0;
|
double _scaleFactor = 3.0;
|
||||||
double _baseScaleFactor = 3.0;
|
double _baseScaleFactor = 3.0;
|
||||||
|
int? _scaleRestoreAssetIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_scrollController = ScrollController(onAttach: _restoreScalePosition);
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
|
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
|
||||||
@@ -154,6 +156,28 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _restoreScalePosition(_) {
|
||||||
|
if (_scaleRestoreAssetIndex == null) return;
|
||||||
|
|
||||||
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
|
asyncSegments.whenData((segments) {
|
||||||
|
final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _scaleRestoreAssetIndex!);
|
||||||
|
if (targetSegment != null) {
|
||||||
|
final assetIndexInSegment = _scaleRestoreAssetIndex! - targetSegment.firstAssetIndex;
|
||||||
|
final newColumnCount = ref.read(timelineArgsProvider).columnCount;
|
||||||
|
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
|
||||||
|
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
|
||||||
|
final targetOffset = targetSegment.indexToLayoutOffset(targetRowIndex);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
_scrollController.jumpTo(targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_scaleRestoreAssetIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
@@ -345,9 +369,28 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final newPerRow = 7 - newScaleFactor.toInt();
|
final newPerRow = 7 - newScaleFactor.toInt();
|
||||||
|
|
||||||
if (newPerRow != _perRow) {
|
if (newPerRow != _perRow) {
|
||||||
|
final currentOffset = _scrollController.offset.clamp(
|
||||||
|
0.0,
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull;
|
||||||
|
int? targetAssetIndex;
|
||||||
|
if (segment != null) {
|
||||||
|
final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset);
|
||||||
|
if (rowIndex > segment.firstIndex) {
|
||||||
|
final rowIndexInSegment = rowIndex - (segment.firstIndex + 1);
|
||||||
|
final assetsPerRow = ref.read(timelineArgsProvider).columnCount;
|
||||||
|
final assetIndexInSegment = rowIndexInSegment * assetsPerRow;
|
||||||
|
targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment;
|
||||||
|
} else {
|
||||||
|
targetAssetIndex = segment.firstAssetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_scaleFactor = newScaleFactor;
|
_scaleFactor = newScaleFactor;
|
||||||
_perRow = newPerRow;
|
_perRow = newPerRow;
|
||||||
|
_scaleRestoreAssetIndex = targetAssetIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||||
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
@@ -138,6 +139,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
|
|
||||||
Future<void> _handleBetaTimelineResume() async {
|
Future<void> _handleBetaTimelineResume() async {
|
||||||
_ref.read(backupProvider.notifier).cancelBackup();
|
_ref.read(backupProvider.notifier).cancelBackup();
|
||||||
|
unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock());
|
||||||
|
|
||||||
// Give isolates time to complete any ongoing database transactions
|
// Give isolates time to complete any ongoing database transactions
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
@@ -209,6 +211,9 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
_pauseOperation = Completer<void>();
|
_pauseOperation = Completer<void>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (Store.isBetaTimelineEnabled) {
|
||||||
|
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
||||||
|
}
|
||||||
await _performPause();
|
await _performPause();
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_log.severe("Error during app pause", e, stackTrace);
|
_log.severe("Error during app pause", e, stackTrace);
|
||||||
@@ -240,6 +245,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
Future<void> handleAppDetached() async {
|
Future<void> handleAppDetached() async {
|
||||||
state = AppLifeCycleEnum.detached;
|
state = AppLifeCycleEnum.detached;
|
||||||
|
|
||||||
|
if (Store.isBetaTimelineEnabled) {
|
||||||
|
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
|
||||||
|
}
|
||||||
|
|
||||||
// Flush logs before closing database
|
// Flush logs before closing database
|
||||||
try {
|
try {
|
||||||
LogService.I.flush();
|
LogService.I.flush();
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class DriftBackupState {
|
|||||||
final int totalCount;
|
final int totalCount;
|
||||||
final int backupCount;
|
final int backupCount;
|
||||||
final int remainderCount;
|
final int remainderCount;
|
||||||
|
final int processingCount;
|
||||||
|
|
||||||
final int enqueueCount;
|
final int enqueueCount;
|
||||||
final int enqueueTotalCount;
|
final int enqueueTotalCount;
|
||||||
@@ -135,6 +136,7 @@ class DriftBackupState {
|
|||||||
required this.totalCount,
|
required this.totalCount,
|
||||||
required this.backupCount,
|
required this.backupCount,
|
||||||
required this.remainderCount,
|
required this.remainderCount,
|
||||||
|
required this.processingCount,
|
||||||
required this.enqueueCount,
|
required this.enqueueCount,
|
||||||
required this.enqueueTotalCount,
|
required this.enqueueTotalCount,
|
||||||
required this.isCanceling,
|
required this.isCanceling,
|
||||||
@@ -145,6 +147,7 @@ class DriftBackupState {
|
|||||||
int? totalCount,
|
int? totalCount,
|
||||||
int? backupCount,
|
int? backupCount,
|
||||||
int? remainderCount,
|
int? remainderCount,
|
||||||
|
int? processingCount,
|
||||||
int? enqueueCount,
|
int? enqueueCount,
|
||||||
int? enqueueTotalCount,
|
int? enqueueTotalCount,
|
||||||
bool? isCanceling,
|
bool? isCanceling,
|
||||||
@@ -154,6 +157,7 @@ class DriftBackupState {
|
|||||||
totalCount: totalCount ?? this.totalCount,
|
totalCount: totalCount ?? this.totalCount,
|
||||||
backupCount: backupCount ?? this.backupCount,
|
backupCount: backupCount ?? this.backupCount,
|
||||||
remainderCount: remainderCount ?? this.remainderCount,
|
remainderCount: remainderCount ?? this.remainderCount,
|
||||||
|
processingCount: processingCount ?? this.processingCount,
|
||||||
enqueueCount: enqueueCount ?? this.enqueueCount,
|
enqueueCount: enqueueCount ?? this.enqueueCount,
|
||||||
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
|
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
|
||||||
isCanceling: isCanceling ?? this.isCanceling,
|
isCanceling: isCanceling ?? this.isCanceling,
|
||||||
@@ -163,7 +167,7 @@ class DriftBackupState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)';
|
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -174,6 +178,7 @@ class DriftBackupState {
|
|||||||
return other.totalCount == totalCount &&
|
return other.totalCount == totalCount &&
|
||||||
other.backupCount == backupCount &&
|
other.backupCount == backupCount &&
|
||||||
other.remainderCount == remainderCount &&
|
other.remainderCount == remainderCount &&
|
||||||
|
other.processingCount == processingCount &&
|
||||||
other.enqueueCount == enqueueCount &&
|
other.enqueueCount == enqueueCount &&
|
||||||
other.enqueueTotalCount == enqueueTotalCount &&
|
other.enqueueTotalCount == enqueueTotalCount &&
|
||||||
other.isCanceling == isCanceling &&
|
other.isCanceling == isCanceling &&
|
||||||
@@ -185,6 +190,7 @@ class DriftBackupState {
|
|||||||
return totalCount.hashCode ^
|
return totalCount.hashCode ^
|
||||||
backupCount.hashCode ^
|
backupCount.hashCode ^
|
||||||
remainderCount.hashCode ^
|
remainderCount.hashCode ^
|
||||||
|
processingCount.hashCode ^
|
||||||
enqueueCount.hashCode ^
|
enqueueCount.hashCode ^
|
||||||
enqueueTotalCount.hashCode ^
|
enqueueTotalCount.hashCode ^
|
||||||
isCanceling.hashCode ^
|
isCanceling.hashCode ^
|
||||||
@@ -203,6 +209,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
backupCount: 0,
|
backupCount: 0,
|
||||||
remainderCount: 0,
|
remainderCount: 0,
|
||||||
|
processingCount: 0,
|
||||||
enqueueCount: 0,
|
enqueueCount: 0,
|
||||||
enqueueTotalCount: 0,
|
enqueueTotalCount: 0,
|
||||||
isCanceling: false,
|
isCanceling: false,
|
||||||
@@ -313,13 +320,14 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getBackupStatus(String userId) async {
|
Future<void> getBackupStatus(String userId) async {
|
||||||
final [totalCount, backupCount, remainderCount] = await Future.wait([
|
final counts = await _uploadService.getBackupCounts(userId);
|
||||||
_uploadService.getBackupTotalCount(),
|
|
||||||
_uploadService.getBackupFinishedCount(userId),
|
|
||||||
_uploadService.getBackupRemainderCount(userId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
state = state.copyWith(totalCount: totalCount, backupCount: backupCount, remainderCount: remainderCount);
|
state = state.copyWith(
|
||||||
|
totalCount: counts.total,
|
||||||
|
backupCount: counts.total - counts.remainder,
|
||||||
|
remainderCount: counts.remainder,
|
||||||
|
processingCount: counts.processing,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startBackup(String userId) {
|
Future<void> startBackup(String userId) {
|
||||||
|
|||||||
@@ -342,11 +342,11 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> shareAssets(ActionSource source) async {
|
Future<ActionResult> shareAssets(ActionSource source, BuildContext context) async {
|
||||||
final ids = _getAssets(source).toList(growable: false);
|
final ids = _getAssets(source).toList(growable: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _service.shareAssets(ids);
|
await _service.shareAssets(ids, context);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Failed to share assets', error, stack);
|
_logger.severe('Failed to share assets', error, stack);
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
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/platform/connectivity_api.g.dart';
|
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/thumbnail_api.g.dart';
|
import 'package:immich_mobile/platform/thumbnail_api.g.dart';
|
||||||
|
|
||||||
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
||||||
|
|
||||||
|
final backgroundWorkerLockServiceProvider = Provider<BackgroundWorkerLockService>(
|
||||||
|
(_) => BackgroundWorkerLockService(BackgroundWorkerLockApi()),
|
||||||
|
);
|
||||||
|
|
||||||
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
||||||
|
|
||||||
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
|
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity;
|
import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity;
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
import 'package:immich_mobile/utils/hash.dart';
|
import 'package:immich_mobile/utils/hash.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)));
|
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)));
|
||||||
@@ -68,7 +70,7 @@ class AssetMediaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this more efficient
|
// TODO: make this more efficient
|
||||||
Future<int> shareAssets(List<BaseAsset> assets) async {
|
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context) async {
|
||||||
final downloadedXFiles = <XFile>[];
|
final downloadedXFiles = <XFile>[];
|
||||||
|
|
||||||
for (var asset in assets) {
|
for (var asset in assets) {
|
||||||
@@ -105,8 +107,12 @@ class AssetMediaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we dont want to await the share result since the
|
// we dont want to await the share result since the
|
||||||
// "preparing" dialog will not disappear unti
|
// "preparing" dialog will not disappear until
|
||||||
Share.shareXFiles(downloadedXFiles).then((result) async {
|
final size = context.sizeData;
|
||||||
|
Share.shareXFiles(
|
||||||
|
downloadedXFiles,
|
||||||
|
sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)),
|
||||||
|
).then((result) async {
|
||||||
for (var file in downloadedXFiles) {
|
for (var file in downloadedXFiles) {
|
||||||
try {
|
try {
|
||||||
await File(file.path).delete();
|
await File(file.path).delete();
|
||||||
|
|||||||
@@ -224,8 +224,8 @@ class ActionService {
|
|||||||
await _assetApiRepository.unStack(stackIds);
|
await _assetApiRepository.unStack(stackIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> shareAssets(List<BaseAsset> assets) {
|
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context) {
|
||||||
return _assetMediaRepository.shareAssets(assets);
|
return _assetMediaRepository.shareAssets(assets, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
|
Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
|
||||||
|
|||||||
@@ -89,16 +89,8 @@ class UploadService {
|
|||||||
return _uploadRepository.getActiveTasks(group);
|
return _uploadRepository.getActiveTasks(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getBackupTotalCount() {
|
Future<({int total, int remainder, int processing})> getBackupCounts(String userId) {
|
||||||
return _backupRepository.getTotalCount();
|
return _backupRepository.getAllCounts(userId);
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> getBackupRemainderCount(String userId) {
|
|
||||||
return _backupRepository.getRemainderCount(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> getBackupFinishedCount(String userId) {
|
|
||||||
return _backupRepository.getBackupCount(userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> manualBackup(List<LocalAsset> localAssets) async {
|
Future<void> manualBackup(List<LocalAsset> localAssets) async {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/provider_utils.dart';
|
import 'package:immich_mobile/utils/provider_utils.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
@@ -193,6 +194,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
if (isBeta) {
|
if (isBeta) {
|
||||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||||
handleSyncFlow();
|
handleSyncFlow();
|
||||||
|
ref.read(websocketProvider.notifier).connect();
|
||||||
context.replaceRoute(const TabShellRoute());
|
context.replaceRoute(const TabShellRoute());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ pigeon:
|
|||||||
dart run pigeon --input pigeon/native_sync_api.dart
|
dart run pigeon --input pigeon/native_sync_api.dart
|
||||||
dart run pigeon --input pigeon/thumbnail_api.dart
|
dart run pigeon --input pigeon/thumbnail_api.dart
|
||||||
dart run pigeon --input pigeon/background_worker_api.dart
|
dart run pigeon --input pigeon/background_worker_api.dart
|
||||||
|
dart run pigeon --input pigeon/background_worker_lock_api.dart
|
||||||
dart run pigeon --input pigeon/connectivity_api.dart
|
dart run pigeon --input pigeon/connectivity_api.dart
|
||||||
dart format lib/platform/native_sync_api.g.dart
|
dart format lib/platform/native_sync_api.g.dart
|
||||||
dart format lib/platform/thumbnail_api.g.dart
|
dart format lib/platform/thumbnail_api.g.dart
|
||||||
dart format lib/platform/background_worker_api.g.dart
|
dart format lib/platform/background_worker_api.g.dart
|
||||||
|
dart format lib/platform/background_worker_lock_api.g.dart
|
||||||
dart format lib/platform/connectivity_api.g.dart
|
dart format lib/platform/connectivity_api.g.dart
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
|
|||||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.142.1
|
- API version: 1.143.0
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|||||||
17
mobile/pigeon/background_worker_lock_api.dart
Normal file
17
mobile/pigeon/background_worker_lock_api.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:pigeon/pigeon.dart';
|
||||||
|
|
||||||
|
@ConfigurePigeon(
|
||||||
|
PigeonOptions(
|
||||||
|
dartOut: 'lib/platform/background_worker_lock_api.g.dart',
|
||||||
|
kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt',
|
||||||
|
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.background', includeErrorClass: false),
|
||||||
|
dartOptions: DartOptions(),
|
||||||
|
dartPackageName: 'immich_mobile',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@HostApi()
|
||||||
|
abstract class BackgroundWorkerLockApi {
|
||||||
|
void lock();
|
||||||
|
|
||||||
|
void unlock();
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.142.1+3015
|
version: 1.143.0+3016
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
|
|||||||
@@ -9858,7 +9858,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.142.1",
|
"version": "1.143.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.142.1",
|
"version": "1.143.0",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.142.1
|
* 1.143.0
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -765,8 +765,8 @@ importers:
|
|||||||
specifier: ~4.8.0
|
specifier: ~4.8.0
|
||||||
version: 4.8.1
|
version: 4.8.1
|
||||||
svelte-gestures:
|
svelte-gestures:
|
||||||
specifier: 5.1.4
|
specifier: ^5.2.2
|
||||||
version: 5.1.4
|
version: 5.2.2
|
||||||
svelte-i18n:
|
svelte-i18n:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1(svelte@5.38.10)
|
version: 4.0.1(svelte@5.38.10)
|
||||||
@@ -10581,8 +10581,8 @@ packages:
|
|||||||
svelte:
|
svelte:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
svelte-gestures@5.1.4:
|
svelte-gestures@5.2.2:
|
||||||
resolution: {integrity: sha512-gfSO/GqWLu9nRMCz12jqdyA0+NTsojYcIBcRqZjwWrpQbqMXr0zWPFpZBtzfYbRHtuFxZImMZp9MrVaFCYbhDg==}
|
resolution: {integrity: sha512-Y+chXPaSx8OsPoFppUwPk8PJzgrZ7xoDJKXeiEc7JBqyKKzXer9hlf8F9O34eFuAWB4/WQEvccACvyBplESL7A==}
|
||||||
|
|
||||||
svelte-i18n@4.0.1:
|
svelte-i18n@4.0.1:
|
||||||
resolution: {integrity: sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==}
|
resolution: {integrity: sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==}
|
||||||
@@ -23862,7 +23862,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.38.10
|
svelte: 5.38.10
|
||||||
|
|
||||||
svelte-gestures@5.1.4: {}
|
svelte-gestures@5.2.2: {}
|
||||||
|
|
||||||
svelte-i18n@4.0.1(svelte@5.38.10):
|
svelte-i18n@4.0.1(svelte@5.38.10):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ghcr.io/immich-app/base-server-dev:202509091104@sha256:4f9275330f1e49e7ce9840758ea91839052fe6ed40972d5bb97a9af857fa956a AS builder
|
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS builder
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
COREPACK_HOME=/tmp
|
COREPACK_HOME=/tmp
|
||||||
@@ -33,7 +33,7 @@ RUN pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install &&
|
|||||||
pnpm --filter @immich/sdk --filter @immich/cli build && \
|
pnpm --filter @immich/sdk --filter @immich/cli build && \
|
||||||
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
||||||
|
|
||||||
FROM ghcr.io/immich-app/base-server-prod:202509091104@sha256:d1ccbac24c84f2f8277cf85281edfca62d85d7daed6a62b8efd3a81bcd3c5e0e
|
FROM ghcr.io/immich-app/base-server-prod:202509210934@sha256:0c7eacf0ba88ca52e1a267cfc62d20d07792ea2c604818c2cbd37dc7dcefdac9
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:202509091104@sha256:4f9275330f1e49e7ce9840758ea91839052fe6ed40972d5bb97a9af857fa956a AS dev
|
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS dev
|
||||||
|
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.142.1",
|
"version": "1.143.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.142.1",
|
"version": "1.143.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"simple-icons": "^15.15.0",
|
"simple-icons": "^15.15.0",
|
||||||
"socket.io-client": "~4.8.0",
|
"socket.io-client": "~4.8.0",
|
||||||
"svelte-gestures": "5.1.4",
|
"svelte-gestures": "^5.2.2",
|
||||||
"svelte-i18n": "^4.0.1",
|
"svelte-i18n": "^4.0.1",
|
||||||
"svelte-maplibre": "^1.2.0",
|
"svelte-maplibre": "^1.2.0",
|
||||||
"svelte-persisted-store": "^0.12.0",
|
"svelte-persisted-store": "^0.12.0",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
@@ -151,7 +152,7 @@
|
|||||||
onclick={onZoomImage}
|
onclick={onZoomImage}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image}
|
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image && $photoViewerImgElement}
|
||||||
<IconButton
|
<IconButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { LoadingSpinner } from '@immich/ui';
|
import { LoadingSpinner } from '@immich/ui';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { swipe, type SwipeCustomEvent } from 'svelte-gestures';
|
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
@@ -92,20 +92,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
copyImage = async () => {
|
copyImage = async () => {
|
||||||
if (!canCopyImageToClipboard()) {
|
if (!canCopyImageToClipboard() || !$photoViewerImgElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl);
|
await copyImageToClipboard($photoViewerImgElement);
|
||||||
if (result.success) {
|
notificationController.show({ type: NotificationType.Info, message: $t('copied_image_to_clipboard') });
|
||||||
notificationController.show({ type: NotificationType.Info, message: $t('copied_image_to_clipboard') });
|
|
||||||
} else {
|
|
||||||
notificationController.show({
|
|
||||||
type: NotificationType.Error,
|
|
||||||
message: $t('errors.clipboard_unsupported_mime_type', { values: { mimeType: result.mimeType } }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('copy_error'));
|
handleError(error, $t('copy_error'));
|
||||||
}
|
}
|
||||||
@@ -241,8 +234,7 @@
|
|||||||
{:else if !imageError}
|
{:else if !imageError}
|
||||||
<div
|
<div
|
||||||
use:zoomImageAction
|
use:zoomImageAction
|
||||||
use:swipe={() => ({})}
|
{...useSwipe(onSwipe)}
|
||||||
onswipe={onSwipe}
|
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
transition:fade={{ duration: haveFadeTransition ? assetViewerFadeDuration : 0 }}
|
transition:fade={{ duration: haveFadeTransition ? assetViewerFadeDuration : 0 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { IconButton, modalManager } from '@immich/ui';
|
import { IconButton, modalManager } from '@immich/ui';
|
||||||
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { swipe } from 'svelte-gestures';
|
import { useSwipe } from 'svelte-gestures';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
@@ -131,6 +131,13 @@
|
|||||||
document.removeEventListener('webkitfullscreenchange', exitFullscreenHandler);
|
document.removeEventListener('webkitfullscreenchange', exitFullscreenHandler);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { swipe, onswipe, onswipedown } = useSwipe(
|
||||||
|
() => {},
|
||||||
|
() => ({ touchAction: 'pan-x' }),
|
||||||
|
{ onswipedown: showControlBar },
|
||||||
|
true,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
@@ -153,7 +160,8 @@
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<svelte:body use:swipe={() => ({ touchAction: 'pan-x' })} onswipedown={showControlBar} />
|
{/* @ts-expect-error https://github.com/Rezi/svelte-gestures/issues/38#issuecomment-3315953573 */ null}
|
||||||
|
<svelte:body {@attach swipe} {onswipe} {onswipedown} />
|
||||||
|
|
||||||
{#if showControls}
|
{#if showControls}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
import { AssetMediaSize } from '@immich/sdk';
|
import { AssetMediaSize } from '@immich/sdk';
|
||||||
import { LoadingSpinner } from '@immich/ui';
|
import { LoadingSpinner } from '@immich/ui';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import type { SwipeCustomEvent } from 'svelte-gestures';
|
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||||
import { swipe } from 'svelte-gestures';
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -130,8 +129,7 @@
|
|||||||
playsinline
|
playsinline
|
||||||
controls
|
controls
|
||||||
class="h-full object-contain"
|
class="h-full object-contain"
|
||||||
use:swipe={() => ({})}
|
{...useSwipe(onSwipe)}
|
||||||
onswipe={onSwipe}
|
|
||||||
oncanplay={(e) => handleCanPlay(e.currentTarget)}
|
oncanplay={(e) => handleCanPlay(e.currentTarget)}
|
||||||
onended={onVideoEnded}
|
onended={onVideoEnded}
|
||||||
onvolumechange={(e) => ($videoViewerMuted = e.currentTarget.muted)}
|
onvolumechange={(e) => ($videoViewerMuted = e.currentTarget.muted)}
|
||||||
|
|||||||
@@ -620,26 +620,7 @@ const imgToBlob = async (imageElement: HTMLImageElement) => {
|
|||||||
throw new Error('Canvas context is null');
|
throw new Error('Canvas context is null');
|
||||||
};
|
};
|
||||||
|
|
||||||
const urlToBlob = async (imageSource: string) => {
|
export const copyImageToClipboard = async (source: HTMLImageElement) => {
|
||||||
const response = await fetch(imageSource);
|
// do not await, so the Safari clipboard write happens in the context of the user gesture
|
||||||
return await response.blob();
|
await navigator.clipboard.write([new ClipboardItem({ ['image/png']: imgToBlob(source) })]);
|
||||||
};
|
|
||||||
|
|
||||||
export const copyImageToClipboard = async (
|
|
||||||
source: HTMLImageElement | string,
|
|
||||||
): Promise<{ success: true } | { success: false; mimeType: string }> => {
|
|
||||||
if (source instanceof HTMLImageElement) {
|
|
||||||
// do not await, so the Safari clipboard write happens in the context of the user gesture
|
|
||||||
await navigator.clipboard.write([new ClipboardItem({ ['image/png']: imgToBlob(source) })]);
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we had a way to get the mime type synchronously, we could do the same thing here
|
|
||||||
const blob = await urlToBlob(source);
|
|
||||||
if (!ClipboardItem.supports(blob.type)) {
|
|
||||||
return { success: false, mimeType: blob.type };
|
|
||||||
}
|
|
||||||
|
|
||||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
|
||||||
return { success: true };
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user