Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eabf535246 | |||
| b0947bf9b9 | |||
| 5ea3b5a5c1 | |||
| ded18f3b6b | |||
| 3724dc465b | |||
| 91b07fbac8 | |||
| fe84f1cc90 | |||
| e748945b4f | |||
| 9f8a7e0bea | |||
| a7719a94fc | |||
| 9a4a320cfb | |||
| 0cce7ebf25 | |||
| b1cdf73a24 | |||
| 147747de32 | |||
| 9abfa6940c | |||
| 39ea73d654 | |||
| 7c1ea2dc73 | |||
| 14169d310a | |||
| 5a1a841365 | |||
| af70111645 | |||
| 8cd3f6b884 | |||
| 124eb8251b | |||
| 529d49471f | |||
| 3868736799 | |||
| 94fc1f213a | |||
| cfc575d89c | |||
| 0b02fda4e0 | |||
| 96516ae4b9 | |||
| caa9b1a041 | |||
| 65dcf9b655 | |||
| 0ceb773865 | |||
| 6995cc2b38 | |||
| 6740c67ed8 | |||
| 4f25cec6df | |||
| ab5dd4d66a | |||
| 7ce8f845b2 | |||
| efe45fd0aa | |||
| 1e6ef5c9e4 | |||
| f53e4721cf | |||
| 7a755a089b | |||
| c468da589a | |||
| b0aafce16b | |||
| de0fd06f43 | |||
| 186b4e1333 | |||
| b74b20824a | |||
| 4a1ff6abce | |||
| edb085691a | |||
| 3e12b10866 | |||
| 4735db8e79 | |||
| b06ea687b4 | |||
| e6bc831c97 | |||
| e73dc3dc72 | |||
| f22338f36f | |||
| 7893dca733 | |||
| c717fd2131 | |||
| a373d50c31 | |||
| cdbc673a59 | |||
| 98cbf94388 | |||
| 15f9ff1fcb | |||
| 92811190a8 | |||
| 6cbdb4c90d | |||
| ba57646f9f | |||
| 7b737786b3 | |||
| d03e97f650 |
@@ -25,6 +25,7 @@ server/upload/
|
|||||||
server/src/queries
|
server/src/queries
|
||||||
server/dist/
|
server/dist/
|
||||||
server/www/
|
server/www/
|
||||||
|
server/resources/v1.pmtiles
|
||||||
|
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/coverage/
|
web/coverage/
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: I have a question or need support
|
- name: ✋ I have a question or need support
|
||||||
url: https://discord.immich.app
|
url: https://discord.immich.app
|
||||||
about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support.
|
about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support.
|
||||||
- name: Feature Request
|
- name: 📷 My photo or video has a date, time, or timezone problem
|
||||||
|
url: https://github.com/immich-app/immich/discussions/12650
|
||||||
|
about: Upload a sample file to this discussion and we will take a look
|
||||||
|
- name: 🌟 Feature request
|
||||||
url: https://github.com/immich-app/immich/discussions/new?category=feature-request
|
url: https://github.com/immich-app/immich/discussions/new?category=feature-request
|
||||||
about: Please use our GitHub Discussion for making feature requests.
|
about: Please use our GitHub Discussion for making feature requests.
|
||||||
- name: I'm unsure where to go
|
- name: 🫣 I'm unsure where to go
|
||||||
url: https://discord.immich.app
|
url: https://discord.immich.app
|
||||||
about: If you are unsure where to go, then joining our Discord is recommended; Just ask!
|
about: If you are unsure where to go, then joining our Discord is recommended; Just ask!
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ jobs:
|
|||||||
run: dart format lib/ --set-exit-if-changed
|
run: dart format lib/ --set-exit-if-changed
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
|
- name: Run dart custom_lint
|
||||||
|
run: dart run custom_lint
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
# Enable after riverpod generator migration is completed
|
# Enable after riverpod generator migration is completed
|
||||||
# - name: Run dart custom lint
|
# - name: Run dart custom lint
|
||||||
# run: dart run custom_lint
|
# run: dart run custom_lint
|
||||||
|
|||||||
Vendored
+4
-4
@@ -5,8 +5,8 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"restart": true,
|
"restart": true,
|
||||||
"port": 9230,
|
"port": 9231,
|
||||||
"name": "Immich Server",
|
"name": "Immich API Server",
|
||||||
"remoteRoot": "/usr/src/app",
|
"remoteRoot": "/usr/src/app",
|
||||||
"localRoot": "${workspaceFolder}/server"
|
"localRoot": "${workspaceFolder}/server"
|
||||||
},
|
},
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"restart": true,
|
"restart": true,
|
||||||
"port": 9231,
|
"port": 9230,
|
||||||
"name": "Immich Microservices",
|
"name": "Immich Workers",
|
||||||
"remoteRoot": "/usr/src/app",
|
"remoteRoot": "/usr/src/app",
|
||||||
"localRoot": "${workspaceFolder}/server"
|
"localRoot": "${workspaceFolder}/server"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+234
-262
@@ -825,9 +825,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.9.1",
|
"version": "9.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
|
||||||
"integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
|
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -844,6 +844,19 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@eslint/plugin-kit": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"levn": "^0.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/module-importer": {
|
"node_modules/@humanwhocodes/module-importer": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||||
@@ -1340,17 +1353,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
|
||||||
"integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
|
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.3.0",
|
"@typescript-eslint/scope-manager": "8.5.0",
|
||||||
"@typescript-eslint/type-utils": "8.3.0",
|
"@typescript-eslint/type-utils": "8.5.0",
|
||||||
"@typescript-eslint/utils": "8.3.0",
|
"@typescript-eslint/utils": "8.5.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.3.1",
|
"ignore": "^5.3.1",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -1374,16 +1387,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
|
||||||
"integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
|
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.3.0",
|
"@typescript-eslint/scope-manager": "8.5.0",
|
||||||
"@typescript-eslint/types": "8.3.0",
|
"@typescript-eslint/types": "8.5.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.3.0",
|
"@typescript-eslint/typescript-estree": "8.5.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1403,14 +1416,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
|
||||||
"integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
|
"integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.3.0",
|
"@typescript-eslint/types": "8.5.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.3.0"
|
"@typescript-eslint/visitor-keys": "8.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1421,14 +1434,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz",
|
||||||
"integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
|
"integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.3.0",
|
"@typescript-eslint/typescript-estree": "8.5.0",
|
||||||
"@typescript-eslint/utils": "8.3.0",
|
"@typescript-eslint/utils": "8.5.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^1.3.0"
|
||||||
},
|
},
|
||||||
@@ -1446,9 +1459,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
|
||||||
"integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
|
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1460,14 +1473,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
|
||||||
"integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
|
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.3.0",
|
"@typescript-eslint/types": "8.5.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@@ -1489,16 +1502,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
|
||||||
"integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
|
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@typescript-eslint/scope-manager": "8.3.0",
|
"@typescript-eslint/scope-manager": "8.5.0",
|
||||||
"@typescript-eslint/types": "8.3.0",
|
"@typescript-eslint/types": "8.5.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.3.0"
|
"@typescript-eslint/typescript-estree": "8.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1512,13 +1525,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.3.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
|
||||||
"integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
|
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.3.0",
|
"@typescript-eslint/types": "8.5.0",
|
||||||
"eslint-visitor-keys": "^3.4.3"
|
"eslint-visitor-keys": "^3.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1530,19 +1543,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
"node_modules/@vitest/coverage-v8": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
|
||||||
"integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
|
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.3.0",
|
"@ampproject/remapping": "^2.3.0",
|
||||||
"@bcoe/v8-coverage": "^0.2.3",
|
"@bcoe/v8-coverage": "^0.2.3",
|
||||||
"debug": "^4.3.5",
|
"debug": "^4.3.6",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"istanbul-lib-report": "^3.0.1",
|
"istanbul-lib-report": "^3.0.1",
|
||||||
"istanbul-lib-source-maps": "^5.0.6",
|
"istanbul-lib-source-maps": "^5.0.6",
|
||||||
"istanbul-reports": "^3.1.7",
|
"istanbul-reports": "^3.1.7",
|
||||||
"magic-string": "^0.30.10",
|
"magic-string": "^0.30.11",
|
||||||
"magicast": "^0.3.4",
|
"magicast": "^0.3.4",
|
||||||
"std-env": "^3.7.0",
|
"std-env": "^3.7.0",
|
||||||
"test-exclude": "^7.0.1",
|
"test-exclude": "^7.0.1",
|
||||||
@@ -1552,17 +1566,24 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vitest": "2.0.5"
|
"@vitest/browser": "2.1.0",
|
||||||
|
"vitest": "2.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
|
||||||
"integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
|
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "2.0.5",
|
"@vitest/spy": "2.1.0",
|
||||||
"@vitest/utils": "2.0.5",
|
"@vitest/utils": "2.1.0",
|
||||||
"chai": "^5.1.1",
|
"chai": "^5.1.1",
|
||||||
"tinyrainbow": "^1.2.0"
|
"tinyrainbow": "^1.2.0"
|
||||||
},
|
},
|
||||||
@@ -1570,11 +1591,40 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
|
||||||
"integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
|
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "^2.1.0-beta.1",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.11"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vitest/spy": "2.1.0",
|
||||||
|
"msw": "^2.3.5",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tinyrainbow": "^1.2.0"
|
"tinyrainbow": "^1.2.0"
|
||||||
},
|
},
|
||||||
@@ -1583,12 +1633,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
|
||||||
"integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
|
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "2.0.5",
|
"@vitest/utils": "2.1.0",
|
||||||
"pathe": "^1.1.2"
|
"pathe": "^1.1.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -1596,24 +1647,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
|
||||||
"integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
|
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "2.0.5",
|
"@vitest/pretty-format": "2.1.0",
|
||||||
"magic-string": "^0.30.10",
|
"magic-string": "^0.30.11",
|
||||||
"pathe": "^1.1.2"
|
"pathe": "^1.1.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
|
||||||
"integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
|
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tinyspy": "^3.0.0"
|
"tinyspy": "^3.0.0"
|
||||||
},
|
},
|
||||||
@@ -1622,13 +1688,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
|
||||||
"integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
|
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "2.0.5",
|
"@vitest/pretty-format": "2.1.0",
|
||||||
"estree-walker": "^3.0.3",
|
|
||||||
"loupe": "^3.1.1",
|
"loupe": "^3.1.1",
|
||||||
"tinyrainbow": "^1.2.0"
|
"tinyrainbow": "^1.2.0"
|
||||||
},
|
},
|
||||||
@@ -1636,6 +1702,19 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/utils/node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.12.1",
|
"version": "8.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
@@ -1710,6 +1789,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -1800,6 +1880,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -1838,6 +1919,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
|
||||||
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
|
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"assertion-error": "^2.0.1",
|
"assertion-error": "^2.0.1",
|
||||||
"check-error": "^2.1.1",
|
"check-error": "^2.1.1",
|
||||||
@@ -1870,6 +1952,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||||
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
@@ -2014,6 +2097,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -2112,9 +2196,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.9.1",
|
"version": "9.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
|
||||||
"integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
|
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2122,7 +2206,8 @@
|
|||||||
"@eslint-community/regexpp": "^4.11.0",
|
"@eslint-community/regexpp": "^4.11.0",
|
||||||
"@eslint/config-array": "^0.18.0",
|
"@eslint/config-array": "^0.18.0",
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "9.9.1",
|
"@eslint/js": "9.10.0",
|
||||||
|
"@eslint/plugin-kit": "^0.1.0",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.3.0",
|
"@humanwhocodes/retry": "^0.3.0",
|
||||||
"@nodelib/fs.walk": "^1.2.8",
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
@@ -2145,7 +2230,6 @@
|
|||||||
"is-glob": "^4.0.0",
|
"is-glob": "^4.0.0",
|
||||||
"is-path-inside": "^3.0.3",
|
"is-path-inside": "^3.0.3",
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
"levn": "^0.4.1",
|
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -2383,6 +2467,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.0"
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -2396,29 +2481,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/execa": {
|
|
||||||
"version": "8.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
|
||||||
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"cross-spawn": "^7.0.3",
|
|
||||||
"get-stream": "^8.0.1",
|
|
||||||
"human-signals": "^5.0.0",
|
|
||||||
"is-stream": "^3.0.0",
|
|
||||||
"merge-stream": "^2.0.0",
|
|
||||||
"npm-run-path": "^5.1.0",
|
|
||||||
"onetime": "^6.0.0",
|
|
||||||
"signal-exit": "^4.1.0",
|
|
||||||
"strip-final-newline": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -2582,22 +2644,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-stream": {
|
|
||||||
"version": "8.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
|
||||||
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
@@ -2688,15 +2739,6 @@
|
|||||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/human-signals": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||||
@@ -2818,18 +2860,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-stream": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -3016,6 +3046,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
|
||||||
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
|
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-func-name": "^2.0.1"
|
"get-func-name": "^2.0.1"
|
||||||
}
|
}
|
||||||
@@ -3061,12 +3092,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merge-stream": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -3087,18 +3112,6 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-fn": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/min-indent": {
|
"node_modules/min-indent": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
@@ -3219,48 +3232,6 @@
|
|||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/npm-run-path": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/npm-run-path/node_modules/path-key": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/onetime": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-fn": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.3",
|
"version": "0.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||||
@@ -3397,21 +3368,23 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pathval": {
|
"node_modules/pathval": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
||||||
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.16"
|
"node": ">= 14.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -3436,9 +3409,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.41",
|
"version": "8.4.47",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3457,8 +3430,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"picocolors": "^1.0.1",
|
"picocolors": "^1.1.0",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
@@ -3827,10 +3800,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3933,18 +3907,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-final-newline": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-indent": {
|
"node_modules/strip-indent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||||
@@ -4031,10 +3993,18 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.8.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
"integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tinypool": {
|
"node_modules/tinypool": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -4055,10 +4025,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyspy": {
|
"node_modules/tinyspy": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
|
||||||
"integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
|
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@@ -4140,9 +4111,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.5.4",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4210,14 +4181,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.2",
|
"version": "5.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||||
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
|
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.43",
|
||||||
"rollup": "^4.20.0"
|
"rollup": "^4.20.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4270,15 +4241,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-node": {
|
"node_modules/vite-node": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz",
|
||||||
"integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
|
"integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"debug": "^4.3.5",
|
"debug": "^4.3.6",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"tinyrainbow": "^1.2.0",
|
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4312,29 +4283,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "2.0.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz",
|
||||||
"integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
|
"integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.3.0",
|
"@vitest/expect": "2.1.0",
|
||||||
"@vitest/expect": "2.0.5",
|
"@vitest/mocker": "2.1.0",
|
||||||
"@vitest/pretty-format": "^2.0.5",
|
"@vitest/pretty-format": "^2.1.0",
|
||||||
"@vitest/runner": "2.0.5",
|
"@vitest/runner": "2.1.0",
|
||||||
"@vitest/snapshot": "2.0.5",
|
"@vitest/snapshot": "2.1.0",
|
||||||
"@vitest/spy": "2.0.5",
|
"@vitest/spy": "2.1.0",
|
||||||
"@vitest/utils": "2.0.5",
|
"@vitest/utils": "2.1.0",
|
||||||
"chai": "^5.1.1",
|
"chai": "^5.1.1",
|
||||||
"debug": "^4.3.5",
|
"debug": "^4.3.6",
|
||||||
"execa": "^8.0.1",
|
"magic-string": "^0.30.11",
|
||||||
"magic-string": "^0.30.10",
|
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"std-env": "^3.7.0",
|
"std-env": "^3.7.0",
|
||||||
"tinybench": "^2.8.0",
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^0.3.0",
|
||||||
"tinypool": "^1.0.0",
|
"tinypool": "^1.0.0",
|
||||||
"tinyrainbow": "^1.2.0",
|
"tinyrainbow": "^1.2.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-node": "2.0.5",
|
"vite-node": "2.1.0",
|
||||||
"why-is-node-running": "^2.3.0"
|
"why-is-node-running": "^2.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4349,8 +4321,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/node": "^18.0.0 || >=20.0.0",
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
"@vitest/browser": "2.0.5",
|
"@vitest/browser": "2.1.0",
|
||||||
"@vitest/ui": "2.0.5",
|
"@vitest/ui": "2.1.0",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
@@ -4535,9 +4507,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
||||||
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ output "immich_app_branch_subdomain" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
output "immich_app_branch_pages_hostname" {
|
output "immich_app_branch_pages_hostname" {
|
||||||
value = cloudflare_record.immich_app_branch_subdomain.value
|
value = cloudflare_record.immich_app_branch_subdomain.content
|
||||||
}
|
}
|
||||||
|
|
||||||
output "pages_project_name" {
|
output "pages_project_name" {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- ../server/resources/v1.pmtiles:/usr/src/app/resources/v1.pmtiles
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -43,6 +44,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001
|
||||||
- 9230:9230
|
- 9230:9230
|
||||||
|
- 9231:9231
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
|
|||||||
@@ -51,5 +51,4 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /usr/lib/wsl:/usr/lib/wsl
|
- /usr/lib/wsl:/usr/lib/wsl
|
||||||
environment:
|
environment:
|
||||||
- LD_LIBRARY_PATH=/usr/lib/wsl/lib
|
|
||||||
- LIBVA_DRIVER_NAME=d3d12
|
- LIBVA_DRIVER_NAME=d3d12
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ Immich supports the option to send notifications via Email for the following eve
|
|||||||
|
|
||||||
## SMTP settings
|
## SMTP settings
|
||||||
|
|
||||||
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
|
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`.
|
||||||
|
|
||||||
Under Email, enter the following details to connect with SMTP servers.
|
Under Email, enter the required details to connect with an SMTP server.
|
||||||
|
|
||||||
You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
|
You can use [this guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
|
||||||
|
|
||||||
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
|
|
||||||
|
|
||||||
## User's notifications settings
|
## User's notifications settings
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 218 KiB |
@@ -64,3 +64,43 @@ Below is an example config for Apache2 site configuration.
|
|||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Traefik Proxy example config
|
||||||
|
|
||||||
|
The example below is for Traefik version 3.
|
||||||
|
|
||||||
|
The most important is to increase the `respondingTimeouts` of the entrypoint used by immich. In this example of entrypoint `websecure` for port `443`. Per default it's set to 60s which leeds to videos stop uploading after 1 minute (Error Code 499). With this config it will fail after 10 minutes which is in most cases enough. Increase it if needed.
|
||||||
|
|
||||||
|
`traefik.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
[...]
|
||||||
|
entryPoints:
|
||||||
|
websecure:
|
||||||
|
address: :443
|
||||||
|
# this section needs to be added
|
||||||
|
transport:
|
||||||
|
respondingTimeouts:
|
||||||
|
readTimeout: 600s
|
||||||
|
idleTimeout: 600s
|
||||||
|
writeTimeout: 600s
|
||||||
|
```
|
||||||
|
|
||||||
|
The second part is in the `docker-compose.yml` file where immich is in. Add the Traefik specific labels like in the example.
|
||||||
|
|
||||||
|
`docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
[...]
|
||||||
|
labels:
|
||||||
|
traefik.enable: true
|
||||||
|
# increase readingTimeouts for the entrypoint used here
|
||||||
|
traefik.http.routers.immich.entrypoints: websecure
|
||||||
|
traefik.http.routers.immich.rule: Host(`immich.your-domain.com`)
|
||||||
|
traefik.http.services.immich.loadbalancer.server.port: 3001
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep in mind, that Traefik needs to communicate with the network where immich is in, usually done
|
||||||
|
by adding the Traefik network to the `immich-server`.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ sidebar_position: 1
|
|||||||
---
|
---
|
||||||
|
|
||||||
import AppArchitecture from './img/app-architecture.png';
|
import AppArchitecture from './img/app-architecture.png';
|
||||||
|
import MobileArchitecture from './img/immich_mobile_architecture.svg';
|
||||||
|
|
||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
@@ -28,7 +29,14 @@ All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for
|
|||||||
|
|
||||||
### Mobile App
|
### Mobile App
|
||||||
|
|
||||||
The mobile app is written in [Flutter](https://flutter.dev/). It uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management.
|
The mobile app is written in [Dart](https://dart.dev/) using [Flutter](https://flutter.dev/). Below is an architecture overview:
|
||||||
|
|
||||||
|
<MobileArchitecture className="p-4 dark:bg-immich-dark-primary my-4" />
|
||||||
|
|
||||||
|
The diagrams shows the target architecture, the current state of the code-base is not always following the architecture yet. New code and contributions should follow this architecture.
|
||||||
|
Currently, it uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management (providers).
|
||||||
|
Entities and Models are the two types of data classes used. While entities are stored in the on-device database, models are ephemeral and only kept in memory.
|
||||||
|
The Repositories should be the only place where other data classes are used internally (such as OpenAPI DTOs). However, their interfaces must not use foreign data classes!
|
||||||
|
|
||||||
### Web Client
|
### Web Client
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" version="24.7.16">
|
||||||
|
<diagram name="Page-1" id="Bp2gX--FtC4sSMWxsLrs">
|
||||||
|
<mxGraphModel dx="1728" dy="954" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="850" pageHeight="1100" background="none" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-1" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.polygon;polyCoords=[[0.25,0],[0.75,0],[1,0.25],[1,0.75],[0.75,1],[0.25,1],[0,0.75],[0,0.25]];polyline=0;strokeWidth=4;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="280" y="217.5" width="465" height="465" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-2" value="<b><font style="font-size: 22px;">Mobile App</font></b>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;rounded=1;fontColor=#ffffff;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="442.5" y="225" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-25" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-4" target="zHhczcy2-Jv_nqmJUiNH-5">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-4" value="Services" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFB400;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="530" y="420" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-26" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-12">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-5" value="Repositories" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E83F7;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="650" y="420" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-24" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-4">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-6" value="Providers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ED79B5;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="410" y="420" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-29" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-8">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-30" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-6">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-7" value="Pages" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="290" y="480" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-31" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-8" target="zHhczcy2-Jv_nqmJUiNH-6">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-8" value="Widgets" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="290" y="360" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-11" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="180" y="368.5" width="81.5" height="163" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-12" value="platform<div>system</div>" style="rhombus;whiteSpace=wrap;html=1;rounded=1;fillColor=#ED79B5;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="800" y="410" width="80" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-13" value="on-device<div>database</div>" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fillColor=#FA2921;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="810" y="310" width="60" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-14" value="server" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;rounded=1;fillColor=#FFB400;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="780" y="500" width="120" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-16" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.07;entryY=0.4;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-14">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-39" value="OpenAPI" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];rounded=1;labelBackgroundColor=#1E83F7;" vertex="1" connectable="0" parent="zHhczcy2-Jv_nqmJUiNH-16">
|
||||||
|
<mxGeometry x="0.0697" y="1" relative="1" as="geometry">
|
||||||
|
<mxPoint x="8" y="10" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-23" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-6">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-27" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=-15;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-13">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-34" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;dashed=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-3">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="810" y="360" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-36" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-9">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="512.08" y="665" as="sourcePoint" />
|
||||||
|
<mxPoint x="512.08" y="265" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-37" value="UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="387.5" y="640" width="70" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-38" value="non-UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="550" y="640" width="90" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-41" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" target="zHhczcy2-Jv_nqmJUiNH-9">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="512.08" y="665" as="sourcePoint" />
|
||||||
|
<mxPoint x="512.08" y="265" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-9" value="Models" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#18C249;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="470" y="510" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="zHhczcy2-Jv_nqmJUiNH-3" value="Entities" style="rounded=1;whiteSpace=wrap;html=1;gradientColor=none;fillColor=#18C249;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="472.5" y="330" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
# Scaling Immich
|
||||||
|
|
||||||
|
Immich is built with modern deployment practices in mind, and the backend is designed to be able to run multiple instances in parallel. When doing this, the only requirement you need to be aware of is that every instance needs to be connected to the shared infrastructure. That means they should all have access to the same Postgres and Redis instances, and have the same files mounted into the containers.
|
||||||
|
|
||||||
|
Scaling can be useful for many reasons. Maybe you have a gaming PC that you want to use for transcoding and thumbnail generation, or perhaps you run a Kubernetes cluster across a handful of powerful servers that you want to make use of.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
If you only have a single machine to run Immich on, scaling to multiple containers is unlikely to provide any benefit. An Immich container will run multiple background tasks at once, and you can increase their number from the admin panel.
|
||||||
|
:::
|
||||||
|
|
||||||
|
The details of how to scale across multiple machines will vary widely between different environments and require some knowledge to set up, and as such this guide gives no specific instructions. In some cases scaling up can be as easy as incrementing the amount of replicas on a Kubernetes deployment, in others it might need you to configure network tunnels or NFS mounts. The details are left as an exercise for the reader ;)
|
||||||
|
|
||||||
|
## Workers
|
||||||
|
|
||||||
|
By default, each running `immich-server` container comes with multiple internal workers. If you're scaling up only to handle more background tasks, you can choose to disable the worker responsible for the API. See [workers](../administration/jobs-workers.md) for more detail.
|
||||||
|
|
||||||
|
## Scaling down
|
||||||
|
|
||||||
|
In the same way you can scale up to multiple containers, you can also choose to scale down. All state is stored in Postgres, Redis, and the filesystem so there is no risk in stopping a running immich-server container, for example if you want to use your GPU to play some games. As long as there is an API worker running you will still be able to browse Immich, and jobs will wait to be processed until there is a worker available for them.
|
||||||
@@ -80,6 +80,10 @@ The Compose file './docker-compose.yml' is invalid because:
|
|||||||
See the previous paragraph about installing from the official docker repository.
|
See the previous paragraph about installing from the official docker repository.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
:::info Health check start interval
|
||||||
|
If you get an error `can't set healthcheck.start_interval as feature require Docker Engine v25 or later`, it helps to comment out the line for `start_interval` in the `database` section of the `docker-compose.yml` file.
|
||||||
|
:::
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
|
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
|
||||||
:::
|
:::
|
||||||
|
|||||||
Generated
+22
-12
@@ -6068,9 +6068,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/docusaurus-lunr-search": {
|
"node_modules/docusaurus-lunr-search": {
|
||||||
"version": "3.4.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.5.0.tgz",
|
||||||
"integrity": "sha512-GfllnNXCLgTSPH9TAKWmbn8VMfwpdOAZ1xl3T2GgX8Pm26qSDLfrrdVwjguaLfMJfzciFL97RKrAJlgrFM48yw==",
|
"integrity": "sha512-k3zN4jYMi/prWInJILGKOxE+BVcgYinwj9+gcECsYm52tS+4ZKzXQzbPnVJAEXmvKOfFMcDFvS3MSmm6cEaxIQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"autocomplete.js": "^0.37.0",
|
"autocomplete.js": "^0.37.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
@@ -6097,14 +6098,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/docusaurus-lunr-search/node_modules/@types/unist": {
|
"node_modules/docusaurus-lunr-search/node_modules/@types/unist": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||||
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
|
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/docusaurus-lunr-search/node_modules/bail": {
|
"node_modules/docusaurus-lunr-search/node_modules/bail": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
|
||||||
"integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
|
"integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
@@ -6114,6 +6117,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -6122,6 +6126,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -6130,6 +6135,7 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
|
||||||
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==",
|
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
@@ -6139,6 +6145,7 @@
|
|||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz",
|
||||||
"integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==",
|
"integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bail": "^1.0.0",
|
"bail": "^1.0.0",
|
||||||
"extend": "^3.0.0",
|
"extend": "^3.0.0",
|
||||||
@@ -6156,6 +6163,7 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
|
||||||
"integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
|
"integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/unist": "^2.0.2"
|
"@types/unist": "^2.0.2"
|
||||||
},
|
},
|
||||||
@@ -6168,6 +6176,7 @@
|
|||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz",
|
||||||
"integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==",
|
"integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/unist": "^2.0.0",
|
"@types/unist": "^2.0.0",
|
||||||
"is-buffer": "^2.0.0",
|
"is-buffer": "^2.0.0",
|
||||||
@@ -6183,6 +6192,7 @@
|
|||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
|
||||||
"integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
|
"integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/unist": "^2.0.0",
|
"@types/unist": "^2.0.0",
|
||||||
"unist-util-stringify-position": "^2.0.0"
|
"unist-util-stringify-position": "^2.0.0"
|
||||||
@@ -16081,9 +16091,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.10",
|
"version": "3.4.11",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
|
||||||
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
|
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
@@ -16443,9 +16453,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.5.4",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ services:
|
|||||||
- IMMICH_METRICS=true
|
- IMMICH_METRICS=true
|
||||||
- IMMICH_ENV=testing
|
- IMMICH_ENV=testing
|
||||||
volumes:
|
volumes:
|
||||||
- upload:/usr/src/app/upload
|
|
||||||
- ./test-assets:/test-assets
|
- ./test-assets:/test-assets
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'auth-server:host-gateway'
|
- 'auth-server:host-gateway'
|
||||||
@@ -44,7 +43,3 @@ services:
|
|||||||
POSTGRES_DB: immich
|
POSTGRES_DB: immich
|
||||||
ports:
|
ports:
|
||||||
- 5435:5432
|
- 5435:5432
|
||||||
|
|
||||||
volumes:
|
|
||||||
model-cache:
|
|
||||||
upload:
|
|
||||||
|
|||||||
Generated
+379
-354
File diff suppressed because it is too large
Load Diff
@@ -53,8 +53,10 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'docker compose up --build -V --remove-orphans',
|
command: 'docker compose up --build --renew-anon-volumes --force-recreate --remove-orphans',
|
||||||
url: 'http://127.0.0.1:2285',
|
url: 'http://127.0.0.1:2285',
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { basename, join } from 'node:path';
|
import { basename, join } from 'node:path';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { createUserDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, testAssetDir, utils } from 'src/utils';
|
import { app, testAssetDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
@@ -11,18 +10,13 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|||||||
describe('/map', () => {
|
describe('/map', () => {
|
||||||
let websocket: Socket;
|
let websocket: Socket;
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonAdmin: LoginResponseDto;
|
|
||||||
let asset: AssetMediaResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
|
||||||
|
|
||||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||||
|
|
||||||
asset = await utils.createAsset(admin.accessToken);
|
|
||||||
|
|
||||||
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
|
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
|
||||||
utils.resetEvents();
|
utils.resetEvents();
|
||||||
const uploadFile = async (input: string) => {
|
const uploadFile = async (input: string) => {
|
||||||
@@ -103,63 +97,6 @@ describe('/map', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /map/style.json', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/map/style.json');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow shared link access', async () => {
|
|
||||||
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: [asset.id],
|
|
||||||
});
|
|
||||||
const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if a theme is not light or dark', async () => {
|
|
||||||
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the light style.json', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme: 'light' })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the dark style.json', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme: 'dark' })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not require admin authentication', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/map/style.json')
|
|
||||||
.query({ theme: 'dark' })
|
|
||||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /map/reverse-geocode', () => {
|
describe('GET /map/reverse-geocode', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/map/reverse-geocode');
|
const { status, body } = await request(app).get('/map/reverse-geocode');
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ describe('/server-info', () => {
|
|||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ describe('/server', () => {
|
|||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ describe('/trash', () => {
|
|||||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||||
|
|
||||||
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
expect(status).toBe(204);
|
.post('/trash/empty')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ count: 1 });
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
||||||
|
|
||||||
@@ -51,8 +54,11 @@ describe('/trash', () => {
|
|||||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
|
||||||
|
|
||||||
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
expect(status).toBe(204);
|
.post('/trash/empty')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ count: 1 });
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
||||||
|
|
||||||
@@ -76,8 +82,11 @@ describe('/trash', () => {
|
|||||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||||
|
|
||||||
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
expect(status).toBe(204);
|
.post('/trash/restore')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ count: 1 });
|
||||||
|
|
||||||
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
|
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
|
||||||
@@ -99,11 +108,12 @@ describe('/trash', () => {
|
|||||||
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||||
expect(before.isTrashed).toBe(true);
|
expect(before.isTrashed).toBe(true);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/trash/restore/assets')
|
.post('/trash/restore/assets')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ids: [assetId] });
|
.send({ ids: [assetId] });
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ count: 1 });
|
||||||
|
|
||||||
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||||
expect(after.isTrashed).toBe(false);
|
expect(after.isTrashed).toBe(false);
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const signupResponseDto = {
|
|||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
license: null,
|
license: null,
|
||||||
|
profileChangedAt: expect.any(String),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const setup = async () => {
|
|||||||
|
|
||||||
const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
|
const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
|
||||||
|
|
||||||
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
const command = 'compose up --build --renew-anon-volumes --force-recreate --remove-orphans';
|
||||||
|
const child = spawn('docker', command.split(' '), { stdio: 'pipe' });
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
const input = data.toString();
|
const input = data.toString();
|
||||||
|
|||||||
+1
-2
@@ -156,8 +156,7 @@ export const utils = {
|
|||||||
|
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
if (table === 'system_metadata') {
|
if (table === 'system_metadata') {
|
||||||
// prevent reverse geocoder from being re-initialized
|
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
||||||
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
|
|
||||||
} else {
|
} else {
|
||||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ download:
|
|||||||
locale_code: nb-NO
|
locale_code: nb-NO
|
||||||
- file: mobile/assets/i18n/sv-SE.json
|
- file: mobile/assets/i18n/sv-SE.json
|
||||||
locale_code: sv-SE
|
locale_code: sv-SE
|
||||||
- file: mobile/assets/i18n/mn.json
|
- file: mobile/assets/i18n/mn-MN.json
|
||||||
locale_code: mn
|
locale_code: mn
|
||||||
- file: mobile/assets/i18n/ko-KR.json
|
- file: mobile/assets/i18n/ko-KR.json
|
||||||
locale_code: ko-KR
|
locale_code: ko-KR
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
ARG DEVICE=cpu
|
ARG DEVICE=cpu
|
||||||
|
|
||||||
FROM python:3.11-bookworm@sha256:3cd9b520be95c671135ea1318f32be6912876024ee16d0f472669d3878801651 AS builder-cpu
|
FROM python:3.11-bookworm@sha256:157a371e60389919fe4a72dff71ce86eaa5234f59114c23b0b346d0d02c74d39 AS builder-cpu
|
||||||
|
|
||||||
FROM builder-cpu AS builder-openvino
|
FROM builder-cpu AS builder-openvino
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
|
|||||||
COPY poetry.lock pyproject.toml ./
|
COPY poetry.lock pyproject.toml ./
|
||||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm@sha256:50ec89bdac0a845ec1751f91cb6187a3d8adb2b919d6e82d17acf48d1a9743fc AS prod-cpu
|
FROM python:3.11-slim-bookworm@sha256:669bbd08353610485a94d5d0c976b4b6498c55280fe42c00f7581f85ee9f3121 AS prod-cpu
|
||||||
|
|
||||||
FROM prod-cpu AS prod-openvino
|
FROM prod-cpu AS prod-openvino
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:b10f75974a30a6889b03519ac48d3e1510fd13d0689468c2c443033a15d84f1b AS builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:5f32c5742e2248f2ca07ccae6861371321aba37372bf8e1a80d6f728f1ab4418 AS builder
|
||||||
|
|
||||||
ENV TRANSFORMERS_CACHE=/cache \
|
ENV TRANSFORMERS_CACHE=/cache \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
|||||||
Generated
+34
-34
@@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-slim"
|
name = "fastapi-slim"
|
||||||
version = "0.114.0"
|
version = "0.114.2"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi_slim-0.114.0-py3-none-any.whl", hash = "sha256:83c8e95301c75c6575f7f6c4b885bf42a4c0b4a85e936e2faca25055470d0afe"},
|
{file = "fastapi_slim-0.114.2-py3-none-any.whl", hash = "sha256:52ae76c53a30ad0fa96beb84c1bf4bef9c72e88c2f7c0473e836f01d7ac3ca6b"},
|
||||||
{file = "fastapi_slim-0.114.0.tar.gz", hash = "sha256:2299d5e0b8818f264725bd13dd91c80b904589be06c98c3d8115132576e5e2dd"},
|
{file = "fastapi_slim-0.114.2.tar.gz", hash = "sha256:76d0a450826fb0fa740268be55ef04c44807da87a94fbbf5f16338b5a4a2d321"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "0.24.6"
|
version = "0.25.0"
|
||||||
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"},
|
{file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"},
|
||||||
{file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"},
|
{file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "locust"
|
name = "locust"
|
||||||
version = "2.31.5"
|
version = "2.31.6"
|
||||||
description = "Developer-friendly load testing framework"
|
description = "Developer-friendly load testing framework"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"},
|
{file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"},
|
||||||
{file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"},
|
{file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2473,13 +2473,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.3.2"
|
version = "8.3.3"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2816,13 +2816,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "13.8.0"
|
version = "13.8.1"
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.0"
|
python-versions = ">=3.7.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"},
|
{file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"},
|
||||||
{file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"},
|
{file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2834,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.6.4"
|
version = "0.6.6"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"},
|
{file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"},
|
||||||
{file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"},
|
{file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"},
|
||||||
{file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"},
|
{file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"},
|
||||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"},
|
{file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"},
|
||||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"},
|
{file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"},
|
||||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"},
|
{file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"},
|
||||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"},
|
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"},
|
||||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"},
|
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"},
|
||||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"},
|
{file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"},
|
||||||
{file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"},
|
{file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"},
|
||||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"},
|
{file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"},
|
||||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"},
|
{file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"},
|
||||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"},
|
{file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"},
|
||||||
{file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"},
|
{file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"},
|
||||||
{file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"},
|
{file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"},
|
||||||
{file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"},
|
{file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"},
|
||||||
{file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"},
|
{file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"},
|
||||||
{file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"},
|
{file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.24.0"
|
"flutter": "3.24.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,859 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.63.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"lazycell",
|
||||||
|
"peeking_take_while",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.1.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800"
|
||||||
|
dependencies = [
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cesu8"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmake"
|
||||||
|
version = "0.1.51"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "combine"
|
||||||
|
version = "4.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doc-comment"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum_dispatch"
|
||||||
|
version = "0.3.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.77",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float_next_after"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "intmap"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee87fd093563344074bacf24faa0bb0227fb6969fb223e922db798516de924d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isar"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
"intmap",
|
||||||
|
"isar-core",
|
||||||
|
"itertools",
|
||||||
|
"jni",
|
||||||
|
"ndk-context",
|
||||||
|
"objc",
|
||||||
|
"objc-foundation",
|
||||||
|
"once_cell",
|
||||||
|
"paste",
|
||||||
|
"serde_json",
|
||||||
|
"threadpool",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isar-core"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"enum_dispatch",
|
||||||
|
"float_next_after",
|
||||||
|
"intmap",
|
||||||
|
"itertools",
|
||||||
|
"libc",
|
||||||
|
"mdbx-sys",
|
||||||
|
"once_cell",
|
||||||
|
"paste",
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"snafu",
|
||||||
|
"widestring",
|
||||||
|
"xxhash-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.10.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
|
||||||
|
dependencies = [
|
||||||
|
"cesu8",
|
||||||
|
"combine",
|
||||||
|
"jni-sys",
|
||||||
|
"log",
|
||||||
|
"thiserror",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-sys"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazycell"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.158"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "malloc_buf"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdbx-sys"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cc",
|
||||||
|
"cmake",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndk-context"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||||
|
dependencies = [
|
||||||
|
"malloc_buf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc-foundation"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||||
|
dependencies = [
|
||||||
|
"block",
|
||||||
|
"objc",
|
||||||
|
"objc_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc_id"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||||
|
dependencies = [
|
||||||
|
"objc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "peeking_take_while"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.86"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.210"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.210"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.77",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.128"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6"
|
||||||
|
dependencies = [
|
||||||
|
"doc-comment",
|
||||||
|
"snafu-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu-derive"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.109"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.77"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.77",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "threadpool"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
|
||||||
|
dependencies = [
|
||||||
|
"num_cpus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xxhash-rust"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.77",
|
||||||
|
]
|
||||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.24.0",
|
"dart.flutterSdkPath": ".fvm/versions/3.24.3",
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.fvm": true
|
"**/.fvm": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,8 +36,76 @@ analyzer:
|
|||||||
- openapi/**
|
- openapi/**
|
||||||
- lib/generated_plugin_registrant.dart
|
- lib/generated_plugin_registrant.dart
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- custom_lint
|
- custom_lint
|
||||||
|
|
||||||
|
custom_lint:
|
||||||
|
debug: true
|
||||||
|
rules:
|
||||||
|
- avoid_build_context_in_providers: false
|
||||||
|
- avoid_public_notifier_properties: false
|
||||||
|
- avoid_manual_providers_as_generated_provider_dependency: false
|
||||||
|
- unsupported_provider_value: false
|
||||||
|
- import_rule_photo_manager:
|
||||||
|
message: photo_manager must only be used in MediaRepositories
|
||||||
|
restrict: package:photo_manager
|
||||||
|
allowed:
|
||||||
|
# required / wanted
|
||||||
|
- 'lib/repositories/{album,asset,file}_media.repository.dart'
|
||||||
|
# acceptable exceptions for the time being
|
||||||
|
- lib/entities/asset.entity.dart # to provide local AssetEntity for now
|
||||||
|
- lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager
|
||||||
|
# refactor to make the providers and services testable
|
||||||
|
- lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
|
||||||
|
- lib/services/{background,backup}.service.dart # uses only PMProgressHandler
|
||||||
|
- import_rule_isar:
|
||||||
|
message: isar must only be used in entities and repositories
|
||||||
|
restrict: package:isar
|
||||||
|
allowed:
|
||||||
|
# required / wanted
|
||||||
|
- lib/entities/*.entity.dart
|
||||||
|
- lib/repositories/{album,asset,backup,user}.repository.dart
|
||||||
|
# acceptable exceptions for the time being
|
||||||
|
- integration_test/test_utils/general_helper.dart
|
||||||
|
- lib/main.dart
|
||||||
|
- lib/routing/router.dart
|
||||||
|
- lib/utils/{db,image_url_builder,migration,renderlist_generator}.dart
|
||||||
|
- test/**.dart
|
||||||
|
# refactor to make the providers and services testable
|
||||||
|
- lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart
|
||||||
|
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
|
||||||
|
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart
|
||||||
|
- lib/services/{asset,asset_description,background,backup,backup_verification,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart
|
||||||
|
- lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart
|
||||||
|
|
||||||
|
- import_rule_openapi:
|
||||||
|
message: openapi must only be used through ApiRepositories
|
||||||
|
restrict: package:openapi
|
||||||
|
allowed:
|
||||||
|
# requried / wanted
|
||||||
|
- lib/repositories/album_api.repository.dart
|
||||||
|
# acceptable exceptions for the time being
|
||||||
|
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
|
||||||
|
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||||
|
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||||
|
# refactor
|
||||||
|
- lib/models/activities/activity.model.dart
|
||||||
|
- lib/models/map/map_marker.model.dart
|
||||||
|
- lib/models/search/search_filter.model.dart
|
||||||
|
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
|
||||||
|
- lib/models/shared_link/shared_link.model.dart
|
||||||
|
- lib/pages/search/search_input.page.dart
|
||||||
|
- lib/providers/asset_viewer/asset_people.provider.dart
|
||||||
|
- lib/providers/authentication.provider.dart
|
||||||
|
- lib/providers/image/immich_remote_{image,thumbnail}_provider.dart
|
||||||
|
- lib/providers/map/map_state.provider.dart
|
||||||
|
- lib/providers/search/{people,search,search_filter}.provider.dart
|
||||||
|
- lib/providers/websocket.provider.dart
|
||||||
|
- lib/routing/auth_guard.dart
|
||||||
|
- lib/services/{activity,api,asset,asset_description,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart
|
||||||
|
- lib/widgets/album/album_thumbnail_listtile.dart
|
||||||
|
- lib/widgets/forms/login/login_form.dart
|
||||||
|
- lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
|
||||||
|
|
||||||
dart_code_metrics:
|
dart_code_metrics:
|
||||||
metrics:
|
metrics:
|
||||||
|
|||||||
@@ -589,4 +589,4 @@
|
|||||||
"viewer_remove_from_stack": "Remove from Stack",
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
"viewer_unstack": "Un-Stack"
|
"viewer_unstack": "Un-Stack"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
include: package:lints/recommended.yaml
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:analyzer/error/listener.dart';
|
||||||
|
import 'package:analyzer/error/error.dart' show ErrorSeverity;
|
||||||
|
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||||
|
// ignore: depend_on_referenced_packages
|
||||||
|
import 'package:glob/glob.dart';
|
||||||
|
|
||||||
|
PluginBase createPlugin() => ImmichLinter();
|
||||||
|
|
||||||
|
class ImmichLinter extends PluginBase {
|
||||||
|
@override
|
||||||
|
List<LintRule> getLintRules(CustomLintConfigs configs) {
|
||||||
|
final List<LintRule> rules = [];
|
||||||
|
for (final entry in configs.rules.entries) {
|
||||||
|
if (entry.value.enabled && entry.key.startsWith("import_rule_")) {
|
||||||
|
final code = makeCode(entry.key, entry.value);
|
||||||
|
final allowedPaths = getStrings(entry.value, "allowed");
|
||||||
|
final forbiddenPaths = getStrings(entry.value, "forbidden");
|
||||||
|
final restrict = getStrings(entry.value, "restrict");
|
||||||
|
rules.add(ImportRule(code, buildGlob(allowedPaths),
|
||||||
|
buildGlob(forbiddenPaths), restrict));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
static makeCode(String name, LintOptions options) => LintCode(
|
||||||
|
name: name,
|
||||||
|
problemMessage: options.json["message"] as String,
|
||||||
|
errorSeverity: ErrorSeverity.WARNING,
|
||||||
|
);
|
||||||
|
|
||||||
|
static List<String> getStrings(LintOptions options, String field) {
|
||||||
|
final List<String> result = [];
|
||||||
|
final excludeOption = options.json[field];
|
||||||
|
if (excludeOption is String) {
|
||||||
|
result.add(excludeOption);
|
||||||
|
} else if (excludeOption is List) {
|
||||||
|
result.addAll(excludeOption.map((option) => option));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Glob? buildGlob(List<String> globs) {
|
||||||
|
if (globs.isEmpty) return null;
|
||||||
|
if (globs.length == 1) return Glob(globs[0], caseSensitive: true);
|
||||||
|
return Glob("{${globs.join(",")}}", caseSensitive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class ImportRule extends DartLintRule {
|
||||||
|
ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict)
|
||||||
|
: super(code: code);
|
||||||
|
|
||||||
|
final Glob? _allowed;
|
||||||
|
final Glob? _forbidden;
|
||||||
|
final List<String> _restrict;
|
||||||
|
int _rootOffset = -1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void run(
|
||||||
|
CustomLintResolver resolver,
|
||||||
|
ErrorReporter reporter,
|
||||||
|
CustomLintContext context,
|
||||||
|
) {
|
||||||
|
if (_rootOffset == -1) {
|
||||||
|
const project = "/immich/mobile/";
|
||||||
|
_rootOffset = resolver.path.indexOf(project) + project.length;
|
||||||
|
}
|
||||||
|
final path = resolver.path.substring(_rootOffset);
|
||||||
|
|
||||||
|
if ((_allowed != null && _allowed!.matches(path)) &&
|
||||||
|
(_forbidden == null || !_forbidden!.matches(path))) return;
|
||||||
|
|
||||||
|
context.registry.addImportDirective((node) {
|
||||||
|
final uri = node.uri.stringValue;
|
||||||
|
if (uri == null) return;
|
||||||
|
for (final restricted in _restrict) {
|
||||||
|
if (uri.startsWith(restricted) == true) {
|
||||||
|
reporter.atNode(node, code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "73.0.0"
|
||||||
|
_macros:
|
||||||
|
dependency: transitive
|
||||||
|
description: dart
|
||||||
|
source: sdk
|
||||||
|
version: "0.3.2"
|
||||||
|
analyzer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.8.0"
|
||||||
|
analyzer_plugin:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: analyzer_plugin
|
||||||
|
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.3"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
ci:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ci
|
||||||
|
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.0"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.5"
|
||||||
|
custom_lint:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: custom_lint
|
||||||
|
sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
custom_lint_builder:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: custom_lint_builder
|
||||||
|
sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
custom_lint_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: custom_lint_core
|
||||||
|
sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.5"
|
||||||
|
dart_style:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_style
|
||||||
|
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.7"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
freezed_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: freezed_annotation
|
||||||
|
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.4"
|
||||||
|
glob:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
hotreloader:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hotreloader
|
||||||
|
sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.0"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.9.0"
|
||||||
|
lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
macros:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: macros
|
||||||
|
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2-main.4"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.16+1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.15.0"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.0"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
pubspec_parse:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pubspec_parse
|
||||||
|
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.28.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.3"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.2.5"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.4.0 <4.0.0"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: immich_mobile_immich_lint
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
analyzer: ^6.8.0
|
||||||
|
analyzer_plugin: ^0.11.3
|
||||||
|
custom_lint_builder: ^0.6.4
|
||||||
|
glob: ^2.1.2
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^4.0.0
|
||||||
@@ -401,7 +401,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 173;
|
CURRENT_PROJECT_VERSION = 175;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 173;
|
CURRENT_PROJECT_VERSION = 175;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -571,7 +571,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 173;
|
CURRENT_PROJECT_VERSION = 175;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -58,11 +58,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.114.0</string>
|
<string>1.115.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>173</string>
|
<string>175</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import 'package:isar/src/common/isar_links_common.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
part 'album.entity.g.dart';
|
part 'album.entity.g.dart';
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ class Album {
|
|||||||
required this.activityEnabled,
|
required this.activityEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fields stored in DB
|
||||||
Id id = Isar.autoIncrement;
|
Id id = Isar.autoIncrement;
|
||||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||||
String? remoteId;
|
String? remoteId;
|
||||||
@@ -43,6 +44,17 @@ class Album {
|
|||||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||||
|
|
||||||
|
// transient fields
|
||||||
|
@ignore
|
||||||
|
bool isAll = false;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
String? remoteThumbnailAssetId;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
int remoteAssetCount = 0;
|
||||||
|
|
||||||
|
// getters
|
||||||
@ignore
|
@ignore
|
||||||
bool get isRemote => remoteId != null;
|
bool get isRemote => remoteId != null;
|
||||||
|
|
||||||
@@ -70,6 +82,21 @@ class Album {
|
|||||||
return name.join(' ');
|
return name.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
String get eTagKeyAssetCount => "device-album-$localId-asset-count";
|
||||||
|
|
||||||
|
// the following getter are needed because Isar links do not make data
|
||||||
|
// accessible in an object freshly created (not loaded from DB)
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
Iterable<User> get remoteUsers => sharedUsers.isEmpty
|
||||||
|
? (sharedUsers as IsarLinksCommon<User>).addedObjects
|
||||||
|
: sharedUsers;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
Iterable<Asset> get remoteAssets =>
|
||||||
|
assets.isEmpty ? (assets as IsarLinksCommon<Asset>).addedObjects : assets;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(other) {
|
bool operator ==(other) {
|
||||||
if (other is! Album) return false;
|
if (other is! Album) return false;
|
||||||
@@ -112,19 +139,6 @@ class Album {
|
|||||||
sharedUsers.length.hashCode ^
|
sharedUsers.length.hashCode ^
|
||||||
assets.length.hashCode;
|
assets.length.hashCode;
|
||||||
|
|
||||||
static Album local(AssetPathEntity ape) {
|
|
||||||
final Album a = Album(
|
|
||||||
name: ape.name,
|
|
||||||
createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
|
||||||
modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
|
||||||
shared: false,
|
|
||||||
activityEnabled: false,
|
|
||||||
);
|
|
||||||
a.owner.value = Store.get(StoreKey.currentUser);
|
|
||||||
a.localId = ape.id;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<Album> remote(AlbumResponseDto dto) async {
|
static Future<Album> remote(AlbumResponseDto dto) async {
|
||||||
final Isar db = Isar.getInstance()!;
|
final Isar db = Isar.getInstance()!;
|
||||||
final Album a = Album(
|
final Album a = Album(
|
||||||
@@ -138,6 +152,7 @@ class Album {
|
|||||||
endDate: dto.endDate,
|
endDate: dto.endDate,
|
||||||
activityEnabled: dto.isActivityEnabled,
|
activityEnabled: dto.isActivityEnabled,
|
||||||
);
|
);
|
||||||
|
a.remoteAssetCount = dto.assetCount;
|
||||||
a.owner.value = await db.users.getById(dto.ownerId);
|
a.owner.value = await db.users.getById(dto.ownerId);
|
||||||
if (dto.albumThumbnailAssetId != null) {
|
if (dto.albumThumbnailAssetId != null) {
|
||||||
a.thumbnail.value = await db.assets
|
a.thumbnail.value = await db.assets
|
||||||
@@ -164,19 +179,12 @@ class Album {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension AssetsHelper on IsarCollection<Album> {
|
extension AssetsHelper on IsarCollection<Album> {
|
||||||
Future<void> store(Album a) async {
|
Future<Album> store(Album a) async {
|
||||||
await put(a);
|
await put(a);
|
||||||
await a.owner.save();
|
await a.owner.save();
|
||||||
await a.thumbnail.save();
|
await a.thumbnail.save();
|
||||||
await a.sharedUsers.save();
|
await a.sharedUsers.save();
|
||||||
await a.assets.save();
|
await a.assets.save();
|
||||||
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
|
||||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AssetPathEntityHelper on AssetPathEntity {
|
|
||||||
String get eTagKeyAssetCount => "device-album-$id-asset-count";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/utils/hash.dart';
|
import 'package:immich_mobile/utils/hash.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' show AssetEntity;
|
||||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
@@ -42,33 +41,6 @@ class Asset {
|
|||||||
stackId = remote.stack?.id,
|
stackId = remote.stack?.id,
|
||||||
thumbhash = remote.thumbhash;
|
thumbhash = remote.thumbhash;
|
||||||
|
|
||||||
Asset.local(AssetEntity local, List<int> hash)
|
|
||||||
: localId = local.id,
|
|
||||||
checksum = base64.encode(hash),
|
|
||||||
durationInSeconds = local.duration,
|
|
||||||
type = AssetType.values[local.typeInt],
|
|
||||||
height = local.height,
|
|
||||||
width = local.width,
|
|
||||||
fileName = local.title!,
|
|
||||||
ownerId = Store.get(StoreKey.currentUser).isarId,
|
|
||||||
fileModifiedAt = local.modifiedDateTime,
|
|
||||||
updatedAt = local.modifiedDateTime,
|
|
||||||
isFavorite = local.isFavorite,
|
|
||||||
isArchived = false,
|
|
||||||
isTrashed = false,
|
|
||||||
isOffline = false,
|
|
||||||
stackCount = 0,
|
|
||||||
fileCreatedAt = local.createDateTime {
|
|
||||||
if (fileCreatedAt.year == 1970) {
|
|
||||||
fileCreatedAt = fileModifiedAt;
|
|
||||||
}
|
|
||||||
if (local.latitude != null) {
|
|
||||||
exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
|
|
||||||
}
|
|
||||||
_local = local;
|
|
||||||
assert(hash.length == 20, "invalid SHA1 hash");
|
|
||||||
}
|
|
||||||
|
|
||||||
Asset({
|
Asset({
|
||||||
this.id = Isar.autoIncrement,
|
this.id = Isar.autoIncrement,
|
||||||
required this.checksum,
|
required this.checksum,
|
||||||
@@ -115,6 +87,8 @@ class Asset {
|
|||||||
return _local;
|
return _local;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set local(AssetEntity? assetEntity) => _local = assetEntity;
|
||||||
|
|
||||||
Id id = Isar.autoIncrement;
|
Id id = Isar.autoIncrement;
|
||||||
|
|
||||||
/// stores the raw SHA1 bytes as a base64 String
|
/// stores the raw SHA1 bytes as a base64 String
|
||||||
@@ -210,6 +184,10 @@ class Asset {
|
|||||||
@ignore
|
@ignore
|
||||||
Duration get duration => Duration(seconds: durationInSeconds);
|
Duration get duration => Duration(seconds: durationInSeconds);
|
||||||
|
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@ignore
|
||||||
|
set byteHash(List<int> hash) => checksum = base64.encode(hash);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(other) {
|
bool operator ==(other) {
|
||||||
if (other is! Asset) return false;
|
if (other is! Asset) return false;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IAlbumRepository {
|
||||||
|
Future<int> count({bool? local});
|
||||||
|
Future<Album> create(Album album);
|
||||||
|
Future<Album?> getById(int id);
|
||||||
|
Future<Album?> getByName(
|
||||||
|
String name, {
|
||||||
|
bool? shared,
|
||||||
|
bool? remote,
|
||||||
|
});
|
||||||
|
Future<Album> update(Album album);
|
||||||
|
Future<void> delete(int albumId);
|
||||||
|
Future<List<Album>> getAll({bool? shared});
|
||||||
|
Future<void> removeUsers(Album album, List<User> users);
|
||||||
|
Future<void> addAssets(Album album, List<Asset> assets);
|
||||||
|
Future<void> removeAssets(Album album, List<Asset> assets);
|
||||||
|
Future<Album> recalculateMetadata(Album album);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IAlbumApiRepository {
|
||||||
|
Future<Album> get(String id);
|
||||||
|
|
||||||
|
Future<List<Album>> getAll({bool? shared});
|
||||||
|
|
||||||
|
Future<Album> create(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
Iterable<String> sharedUserIds = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Album> update(
|
||||||
|
String albumId, {
|
||||||
|
String? name,
|
||||||
|
String? thumbnailAssetId,
|
||||||
|
String? description,
|
||||||
|
bool? activityEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> delete(String albumId);
|
||||||
|
|
||||||
|
Future<({List<String> added, List<String> duplicates})> addAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<({List<String> removed, List<String> failed})> removeAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Album> addUsers(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> userIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId});
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IAlbumMediaRepository {
|
||||||
|
Future<List<Album>> getAll();
|
||||||
|
|
||||||
|
Future<List<String>> getAssetIds(String albumId);
|
||||||
|
|
||||||
|
Future<int> getAssetCount(String albumId);
|
||||||
|
|
||||||
|
Future<List<Asset>> getAssets(
|
||||||
|
String albumId, {
|
||||||
|
int start = 0,
|
||||||
|
int end = 0x7fffffffffffffff,
|
||||||
|
DateTime? modifiedFrom,
|
||||||
|
DateTime? modifiedUntil,
|
||||||
|
bool orderByModificationDate = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Album> get(String id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IAssetRepository {
|
||||||
|
Future<Asset?> getByRemoteId(String id);
|
||||||
|
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
|
||||||
|
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy});
|
||||||
|
Future<void> deleteById(List<int> ids);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IAssetMediaRepository {
|
||||||
|
Future<List<String>> deleteAll(List<String> ids);
|
||||||
|
|
||||||
|
Future<Asset?> get(String id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IBackupRepository {
|
||||||
|
Future<List<String>> getIdsBySelection(BackupSelection backup);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IFileMediaRepository {
|
||||||
|
Future<Asset?> saveImage(
|
||||||
|
Uint8List data, {
|
||||||
|
required String title,
|
||||||
|
String? relativePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Asset?> saveVideo(
|
||||||
|
File file, {
|
||||||
|
required String title,
|
||||||
|
String? relativePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Asset?> saveLivePhoto({
|
||||||
|
required File image,
|
||||||
|
required File video,
|
||||||
|
required String title,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> clearFileCache();
|
||||||
|
|
||||||
|
Future<void> enableBackgroundAccess();
|
||||||
|
|
||||||
|
Future<void> requestExtendedPermissions();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IUserRepository {
|
||||||
|
Future<List<User>> getByIds(List<String> ids);
|
||||||
|
Future<User?> get(String id);
|
||||||
|
}
|
||||||
@@ -1,45 +1,47 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
|
||||||
class AvailableAlbum {
|
class AvailableAlbum {
|
||||||
final AssetPathEntity albumEntity;
|
final Album album;
|
||||||
|
final int assetCount;
|
||||||
final DateTime? lastBackup;
|
final DateTime? lastBackup;
|
||||||
AvailableAlbum({
|
AvailableAlbum({
|
||||||
required this.albumEntity,
|
required this.album,
|
||||||
|
required this.assetCount,
|
||||||
this.lastBackup,
|
this.lastBackup,
|
||||||
});
|
});
|
||||||
|
|
||||||
AvailableAlbum copyWith({
|
AvailableAlbum copyWith({
|
||||||
AssetPathEntity? albumEntity,
|
Album? album,
|
||||||
|
int? assetCount,
|
||||||
DateTime? lastBackup,
|
DateTime? lastBackup,
|
||||||
Uint8List? thumbnailData,
|
Uint8List? thumbnailData,
|
||||||
}) {
|
}) {
|
||||||
return AvailableAlbum(
|
return AvailableAlbum(
|
||||||
albumEntity: albumEntity ?? this.albumEntity,
|
album: album ?? this.album,
|
||||||
|
assetCount: assetCount ?? this.assetCount,
|
||||||
lastBackup: lastBackup ?? this.lastBackup,
|
lastBackup: lastBackup ?? this.lastBackup,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get name => albumEntity.name;
|
String get name => album.name;
|
||||||
|
|
||||||
Future<int> get assetCount => albumEntity.assetCountAsync;
|
String get id => album.localId!;
|
||||||
|
|
||||||
String get id => albumEntity.id;
|
bool get isAll => album.isAll;
|
||||||
|
|
||||||
bool get isAll => albumEntity.isAll;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
|
'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is AvailableAlbum && other.albumEntity == albumEntity;
|
return other is AvailableAlbum && other.album == album;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => albumEntity.hashCode;
|
int get hashCode => album.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
class BackupCandidate {
|
class BackupCandidate {
|
||||||
BackupCandidate({required this.asset, required this.albumNames});
|
BackupCandidate({required this.asset, required this.albumNames});
|
||||||
|
|
||||||
AssetEntity asset;
|
Asset asset;
|
||||||
List<String> albumNames;
|
List<String> albumNames;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
class ErrorUploadAsset {
|
class ErrorUploadAsset {
|
||||||
final String id;
|
final String id;
|
||||||
final DateTime fileCreatedAt;
|
final DateTime fileCreatedAt;
|
||||||
final String fileName;
|
final String fileName;
|
||||||
final String fileType;
|
final String fileType;
|
||||||
final AssetEntity asset;
|
final Asset asset;
|
||||||
final String errorMessage;
|
final String errorMessage;
|
||||||
|
|
||||||
const ErrorUploadAsset({
|
const ErrorUploadAsset({
|
||||||
@@ -22,7 +22,7 @@ class ErrorUploadAsset {
|
|||||||
DateTime? fileCreatedAt,
|
DateTime? fileCreatedAt,
|
||||||
String? fileName,
|
String? fileName,
|
||||||
String? fileType,
|
String? fileType,
|
||||||
AssetEntity? asset,
|
Asset? asset,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
return ErrorUploadAsset(
|
return ErrorUploadAsset(
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ class ServerConfig {
|
|||||||
final int trashDays;
|
final int trashDays;
|
||||||
final String oauthButtonText;
|
final String oauthButtonText;
|
||||||
final String externalDomain;
|
final String externalDomain;
|
||||||
|
final String mapDarkStyleUrl;
|
||||||
|
final String mapLightStyleUrl;
|
||||||
|
|
||||||
const ServerConfig({
|
const ServerConfig({
|
||||||
required this.trashDays,
|
required this.trashDays,
|
||||||
required this.oauthButtonText,
|
required this.oauthButtonText,
|
||||||
required this.externalDomain,
|
required this.externalDomain,
|
||||||
|
required this.mapDarkStyleUrl,
|
||||||
|
required this.mapLightStyleUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
ServerConfig copyWith({
|
ServerConfig copyWith({
|
||||||
@@ -20,6 +24,8 @@ class ServerConfig {
|
|||||||
trashDays: trashDays ?? this.trashDays,
|
trashDays: trashDays ?? this.trashDays,
|
||||||
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
|
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
|
||||||
externalDomain: externalDomain ?? this.externalDomain,
|
externalDomain: externalDomain ?? this.externalDomain,
|
||||||
|
mapDarkStyleUrl: mapDarkStyleUrl,
|
||||||
|
mapLightStyleUrl: mapLightStyleUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +36,9 @@ class ServerConfig {
|
|||||||
ServerConfig.fromDto(ServerConfigDto dto)
|
ServerConfig.fromDto(ServerConfigDto dto)
|
||||||
: trashDays = dto.trashDays,
|
: trashDays = dto.trashDays,
|
||||||
oauthButtonText = dto.oauthButtonText,
|
oauthButtonText = dto.oauthButtonText,
|
||||||
externalDomain = dto.externalDomain;
|
externalDomain = dto.externalDomain,
|
||||||
|
mapDarkStyleUrl = dto.mapDarkStyleUrl,
|
||||||
|
mapLightStyleUrl = dto.mapLightStyleUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant ServerConfig other) {
|
bool operator ==(covariant ServerConfig other) {
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class AlbumPreviewPage extends HookConsumerWidget {
|
class AlbumPreviewPage extends HookConsumerWidget {
|
||||||
final AssetPathEntity album;
|
final Album album;
|
||||||
const AlbumPreviewPage({super.key, required this.album});
|
const AlbumPreviewPage({super.key, required this.album});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final assets = useState<List<AssetEntity>>([]);
|
final assets = useState<List<Asset>>([]);
|
||||||
|
|
||||||
getAssetsInAlbum() async {
|
getAssetsInAlbum() async {
|
||||||
assets.value = await album.getAssetListRange(
|
assets.value = await ref
|
||||||
start: 0,
|
.read(albumMediaRepositoryProvider)
|
||||||
end: await album.assetCountAsync,
|
.getAssets(album.localId!);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -68,30 +67,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
itemCount: assets.value.length,
|
itemCount: assets.value.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
Future<Uint8List?> thumbData =
|
return ImmichThumbnail(
|
||||||
assets.value[index].thumbnailDataWithSize(
|
asset: assets.value[index],
|
||||||
const ThumbnailSize(200, 200),
|
width: 100,
|
||||||
quality: 50,
|
height: 100,
|
||||||
);
|
|
||||||
|
|
||||||
return FutureBuilder<Uint8List?>(
|
|
||||||
future: thumbData,
|
|
||||||
builder: ((context, snapshot) {
|
|
||||||
if (snapshot.hasData && snapshot.data != null) {
|
|
||||||
return Image.memory(
|
|
||||||
snapshot.data!,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class FailedBackupStatusPage extends HookConsumerWidget {
|
class FailedBackupStatusPage extends HookConsumerWidget {
|
||||||
@@ -70,11 +69,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Image(
|
child: Image(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
image: AssetEntityImageProvider(
|
image: ImmichLocalThumbnailProvider(
|
||||||
errorAsset.asset,
|
asset: errorAsset.asset,
|
||||||
isOriginal: false,
|
height: 512,
|
||||||
thumbnailSize: const ThumbnailSize.square(512),
|
width: 512,
|
||||||
thumbnailFormat: ThumbnailFormat.jpeg,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -67,10 +67,10 @@ class EditImagePage extends ConsumerWidget {
|
|||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final Uint8List imageData = await _imageToUint8List(image);
|
final Uint8List imageData = await _imageToUint8List(image);
|
||||||
await PhotoManager.editor.saveImage(
|
await ref.read(fileMediaRepositoryProvider).saveImage(
|
||||||
imageData,
|
imageData,
|
||||||
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
||||||
);
|
);
|
||||||
await ref.read(albumProvider.notifier).getDeviceAlbums();
|
await ref.read(albumProvider.notifier).getDeviceAlbums();
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.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';
|
||||||
import 'package:immich_mobile/providers/favorite_provider.dart';
|
import 'package:immich_mobile/providers/favorite.provider.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -7,27 +8,27 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||||
import 'package:immich_mobile/models/map/map_event.model.dart';
|
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_marker.provider.dart';
|
import 'package:immich_mobile/providers/map/map_marker.provider.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/utils/map_utils.dart';
|
import 'package:immich_mobile/utils/map_utils.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@@ -304,7 +305,7 @@ class MapPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
bottom: MediaQuery.paddingOf(context).bottom + 16,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: onZoomToLocation,
|
onPressed: onZoomToLocation,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
@@ -15,7 +16,6 @@ import 'package:immich_mobile/utils/db.dart';
|
|||||||
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<bool> {
|
class AssetNotifier extends StateNotifier<bool> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
@@ -257,7 +257,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
// Delete asset from device
|
// Delete asset from device
|
||||||
if (local.isNotEmpty) {
|
if (local.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
return await PhotoManager.editor.deleteWithIds(local);
|
return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
log.severe("Failed to delete asset from device", e, stack);
|
log.severe("Failed to delete asset from device", e, stack);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/models/backup/available_album.model.dart';
|
import 'package:immich_mobile/models/backup/available_album.model.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
@@ -13,6 +16,8 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
|||||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
|
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
|
||||||
@@ -28,7 +33,7 @@ import 'package:immich_mobile/utils/diff.dart';
|
|||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
BackupNotifier(
|
BackupNotifier(
|
||||||
@@ -38,6 +43,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
this._backgroundService,
|
this._backgroundService,
|
||||||
this._galleryPermissionNotifier,
|
this._galleryPermissionNotifier,
|
||||||
this._db,
|
this._db,
|
||||||
|
this._albumMediaRepository,
|
||||||
|
this._fileMediaRepository,
|
||||||
this.ref,
|
this.ref,
|
||||||
) : super(
|
) : super(
|
||||||
BackUpState(
|
BackUpState(
|
||||||
@@ -86,6 +93,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final BackgroundService _backgroundService;
|
final BackgroundService _backgroundService;
|
||||||
final GalleryPermissionNotifier _galleryPermissionNotifier;
|
final GalleryPermissionNotifier _galleryPermissionNotifier;
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -224,22 +233,24 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Stopwatch stopwatch = Stopwatch()..start();
|
Stopwatch stopwatch = Stopwatch()..start();
|
||||||
// Get all albums on the device
|
// Get all albums on the device
|
||||||
List<AvailableAlbum> availableAlbums = [];
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
List<Album> albums = await _albumMediaRepository.getAll();
|
||||||
hasAll: true,
|
|
||||||
type: RequestType.common,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Map of id -> album for quick album lookup later on.
|
// Map of id -> album for quick album lookup later on.
|
||||||
Map<String, AssetPathEntity> albumMap = {};
|
Map<String, Album> albumMap = {};
|
||||||
|
|
||||||
log.info('Found ${albums.length} local albums');
|
log.info('Found ${albums.length} local albums');
|
||||||
|
|
||||||
for (AssetPathEntity album in albums) {
|
for (Album album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(
|
||||||
|
album: album,
|
||||||
|
assetCount: await ref
|
||||||
|
.read(albumMediaRepositoryProvider)
|
||||||
|
.getAssetCount(album.localId!),
|
||||||
|
);
|
||||||
|
|
||||||
availableAlbums.add(availableAlbum);
|
availableAlbums.add(availableAlbum);
|
||||||
|
|
||||||
albumMap[album.id] = album;
|
albumMap[album.localId!] = album;
|
||||||
}
|
}
|
||||||
state = state.copyWith(availableAlbums: availableAlbums);
|
state = state.copyWith(availableAlbums: availableAlbums);
|
||||||
|
|
||||||
@@ -248,14 +259,18 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final List<BackupAlbum> selectedBackupAlbums =
|
final List<BackupAlbum> selectedBackupAlbums =
|
||||||
await _backupService.selectedAlbumsQuery().findAll();
|
await _backupService.selectedAlbumsQuery().findAll();
|
||||||
|
|
||||||
// Generate AssetPathEntity from id to add to local state
|
|
||||||
final Set<AvailableAlbum> selectedAlbums = {};
|
final Set<AvailableAlbum> selectedAlbums = {};
|
||||||
for (final BackupAlbum ba in selectedBackupAlbums) {
|
for (final BackupAlbum ba in selectedBackupAlbums) {
|
||||||
final albumAsset = albumMap[ba.id];
|
final albumAsset = albumMap[ba.id];
|
||||||
|
|
||||||
if (albumAsset != null) {
|
if (albumAsset != null) {
|
||||||
selectedAlbums.add(
|
selectedAlbums.add(
|
||||||
AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
|
AvailableAlbum(
|
||||||
|
album: albumAsset,
|
||||||
|
assetCount:
|
||||||
|
await _albumMediaRepository.getAssetCount(albumAsset.localId!),
|
||||||
|
lastBackup: ba.lastBackup,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log.severe('Selected album not found');
|
log.severe('Selected album not found');
|
||||||
@@ -268,7 +283,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
if (albumAsset != null) {
|
if (albumAsset != null) {
|
||||||
excludedAlbums.add(
|
excludedAlbums.add(
|
||||||
AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
|
AvailableAlbum(
|
||||||
|
album: albumAsset,
|
||||||
|
assetCount: await ref
|
||||||
|
.read(albumMediaRepositoryProvider)
|
||||||
|
.getAssetCount(albumAsset.localId!),
|
||||||
|
lastBackup: ba.lastBackup,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log.severe('Excluded album not found');
|
log.severe('Excluded album not found');
|
||||||
@@ -292,28 +313,32 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Those assets are unique and are used as the total assets
|
/// Those assets are unique and are used as the total assets
|
||||||
///
|
///
|
||||||
Future<void> _updateBackupAssetCount() async {
|
Future<void> _updateBackupAssetCount() async {
|
||||||
|
// Save to persistent storage
|
||||||
|
await _updatePersistentAlbumsSelection();
|
||||||
|
|
||||||
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
|
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
|
||||||
final Set<BackupCandidate> assetsFromSelectedAlbums = {};
|
final Set<BackupCandidate> assetsFromSelectedAlbums = {};
|
||||||
final Set<BackupCandidate> assetsFromExcludedAlbums = {};
|
final Set<BackupCandidate> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
for (final album in state.selectedBackupAlbums) {
|
for (final album in state.selectedBackupAlbums) {
|
||||||
final assetCount = await album.albumEntity.assetCountAsync;
|
final assetCount = await ref
|
||||||
|
.read(albumMediaRepositoryProvider)
|
||||||
|
.getAssetCount(album.album.localId!);
|
||||||
|
|
||||||
if (assetCount == 0) {
|
if (assetCount == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = await album.albumEntity.getAssetListRange(
|
final assets = await ref
|
||||||
start: 0,
|
.read(albumMediaRepositoryProvider)
|
||||||
end: assetCount,
|
.getAssets(album.album.localId!);
|
||||||
);
|
|
||||||
|
|
||||||
// Add album's name to the asset info
|
// Add album's name to the asset info
|
||||||
for (final asset in assets) {
|
for (final asset in assets) {
|
||||||
List<String> albumNames = [album.name];
|
List<String> albumNames = [album.name];
|
||||||
|
|
||||||
final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull(
|
final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull(
|
||||||
(a) => a.asset.id == asset.id,
|
(a) => a.asset.localId == asset.localId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingAsset != null) {
|
if (existingAsset != null) {
|
||||||
@@ -331,16 +356,17 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (final album in state.excludedBackupAlbums) {
|
for (final album in state.excludedBackupAlbums) {
|
||||||
final assetCount = await album.albumEntity.assetCountAsync;
|
final assetCount = await ref
|
||||||
|
.read(albumMediaRepositoryProvider)
|
||||||
|
.getAssetCount(album.album.localId!);
|
||||||
|
|
||||||
if (assetCount == 0) {
|
if (assetCount == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = await album.albumEntity.getAssetListRange(
|
final assets = await ref
|
||||||
start: 0,
|
.read(albumMediaRepositoryProvider)
|
||||||
end: assetCount,
|
.getAssets(album.album.localId!);
|
||||||
);
|
|
||||||
|
|
||||||
for (final asset in assets) {
|
for (final asset in assets) {
|
||||||
assetsFromExcludedAlbums.add(
|
assetsFromExcludedAlbums.add(
|
||||||
@@ -360,14 +386,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Find asset that were backup from selected albums
|
// Find asset that were backup from selected albums
|
||||||
final Set<String> selectedAlbumsBackupAssets =
|
final Set<String> selectedAlbumsBackupAssets =
|
||||||
Set.from(allUniqueAssets.map((e) => e.asset.id));
|
Set.from(allUniqueAssets.map((e) => e.asset.localId));
|
||||||
|
|
||||||
selectedAlbumsBackupAssets
|
selectedAlbumsBackupAssets
|
||||||
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
||||||
|
|
||||||
// Remove duplicated asset from all unique assets
|
// Remove duplicated asset from all unique assets
|
||||||
allUniqueAssets.removeWhere(
|
allUniqueAssets.removeWhere(
|
||||||
(candidate) => duplicatedAssetIds.contains(candidate.asset.id),
|
(candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (allUniqueAssets.isEmpty) {
|
if (allUniqueAssets.isEmpty) {
|
||||||
@@ -385,9 +411,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to persistent storage
|
|
||||||
await _updatePersistentAlbumsSelection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all necessary information for calculating the available albums,
|
/// Get all necessary information for calculating the available albums,
|
||||||
@@ -454,7 +477,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
final hasPermission = _galleryPermissionNotifier.hasPermission;
|
final hasPermission = _galleryPermissionNotifier.hasPermission;
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
await PhotoManager.clearFileCache();
|
await _fileMediaRepository.clearFileCache();
|
||||||
|
|
||||||
if (state.allUniqueAssets.isEmpty) {
|
if (state.allUniqueAssets.isEmpty) {
|
||||||
log.info("No Asset On Device - Abort Backup Process");
|
log.info("No Asset On Device - Abort Backup Process");
|
||||||
@@ -465,7 +488,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Set<BackupCandidate> assetsWillBeBackup = Set.from(state.allUniqueAssets);
|
Set<BackupCandidate> assetsWillBeBackup = Set.from(state.allUniqueAssets);
|
||||||
// Remove item that has already been backed up
|
// Remove item that has already been backed up
|
||||||
for (final assetId in state.allAssetsInDatabase) {
|
for (final assetId in state.allAssetsInDatabase) {
|
||||||
assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId);
|
assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetsWillBeBackup.isEmpty) {
|
if (assetsWillBeBackup.isEmpty) {
|
||||||
@@ -531,7 +554,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
allUniqueAssets: state.allUniqueAssets
|
allUniqueAssets: state.allUniqueAssets
|
||||||
.where(
|
.where(
|
||||||
(candidate) => candidate.asset.id != result.candidate.asset.id,
|
(candidate) =>
|
||||||
|
candidate.asset.localId != result.candidate.asset.localId,
|
||||||
)
|
)
|
||||||
.toSet(),
|
.toSet(),
|
||||||
);
|
);
|
||||||
@@ -539,11 +563,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAlbumsBackupAssetsIds: {
|
selectedAlbumsBackupAssetsIds: {
|
||||||
...state.selectedAlbumsBackupAssetsIds,
|
...state.selectedAlbumsBackupAssetsIds,
|
||||||
result.candidate.asset.id,
|
result.candidate.asset.localId!,
|
||||||
},
|
},
|
||||||
allAssetsInDatabase: [
|
allAssetsInDatabase: [
|
||||||
...state.allAssetsInDatabase,
|
...state.allAssetsInDatabase,
|
||||||
result.candidate.asset.id,
|
result.candidate.asset.localId!,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -552,7 +576,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
state.selectedAlbumsBackupAssetsIds.length ==
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
0) {
|
0) {
|
||||||
final latestAssetBackup = state.allUniqueAssets
|
final latestAssetBackup = state.allUniqueAssets
|
||||||
.map((candidate) => candidate.asset.modifiedDateTime)
|
.map((candidate) => candidate.asset.fileModifiedAt)
|
||||||
.reduce(
|
.reduce(
|
||||||
(v, e) => e.isAfter(v) ? e : v,
|
(v, e) => e.isAfter(v) ? e : v,
|
||||||
);
|
);
|
||||||
@@ -741,6 +765,8 @@ final backupProvider =
|
|||||||
ref.watch(backgroundServiceProvider),
|
ref.watch(backgroundServiceProvider),
|
||||||
ref.watch(galleryPermissionNotifier.notifier),
|
ref.watch(galleryPermissionNotifier.notifier),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
ref,
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||||
@@ -27,7 +28,7 @@ import 'package:immich_mobile/utils/backup_progress.dart';
|
|||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||||
|
|
||||||
final manualUploadProvider =
|
final manualUploadProvider =
|
||||||
StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
|
StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
|
||||||
@@ -193,17 +194,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
|
_backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
|
||||||
|
|
||||||
if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
|
if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
|
||||||
await PhotoManager.clearFileCache();
|
await ref.read(fileMediaRepositoryProvider).clearFileCache();
|
||||||
|
|
||||||
// We do not have 1:1 mapping of all AssetEntity fields to Asset. This results in cases
|
final allAssetsFromDevice =
|
||||||
// where platform specific fields such as `subtype` used to detect platform specific assets such as
|
allManualUploads.where((e) => e.isLocal && !e.isRemote).toList();
|
||||||
// LivePhoto in iOS is lost when we directly fetch the local asset from Asset using Asset.local
|
|
||||||
List<AssetEntity?> allAssetsFromDevice = await Future.wait(
|
|
||||||
allManualUploads
|
|
||||||
// Filter local only assets
|
|
||||||
.where((e) => e.isLocal && !e.isRemote)
|
|
||||||
.map((e) => e.local!.obtainForNewProperties()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allAssetsFromDevice.length != allManualUploads.length) {
|
if (allAssetsFromDevice.length != allManualUploads.length) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
@@ -221,11 +215,17 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||||||
await _backupService.buildUploadCandidates(
|
await _backupService.buildUploadCandidates(
|
||||||
selectedBackupAlbums,
|
selectedBackupAlbums,
|
||||||
excludedBackupAlbums,
|
excludedBackupAlbums,
|
||||||
|
useTimeFilter: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extrack candidate from allAssetsFromDevice.nonNulls
|
// Extrack candidate from allAssetsFromDevice
|
||||||
final uploadAssets = candidates
|
final uploadAssets = candidates.where(
|
||||||
.where((e) => allAssetsFromDevice.nonNulls.contains(e.asset));
|
(candidate) =>
|
||||||
|
allAssetsFromDevice.firstWhereOrNull(
|
||||||
|
(asset) => asset.localId == candidate.asset.localId,
|
||||||
|
) !=
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
if (uploadAssets.isEmpty) {
|
if (uploadAssets.isEmpty) {
|
||||||
debugPrint("[_startUpload] No Assets to upload - Abort Process");
|
debugPrint("[_startUpload] No Assets to upload - Abort Process");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:flutter/painting.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
|
||||||
|
|
||||||
/// The local image provider for an asset
|
/// The local image provider for an asset
|
||||||
class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
|
class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
|
||||||
|
|
||||||
/// The local image provider for an asset
|
/// The local image provider for an asset
|
||||||
/// Only viable
|
/// Only viable
|
||||||
|
|||||||
@@ -1,28 +1,23 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
|
||||||
import 'package:immich_mobile/models/map/map_state.model.dart';
|
import 'package:immich_mobile/models/map/map_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'map_state.provider.g.dart';
|
part 'map_state.provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class MapStateNotifier extends _$MapStateNotifier {
|
class MapStateNotifier extends _$MapStateNotifier {
|
||||||
final _log = Logger("MapStateNotifier");
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MapState build() {
|
MapState build() {
|
||||||
final appSettingsProvider = ref.read(appSettingsServiceProvider);
|
final appSettingsProvider = ref.read(appSettingsServiceProvider);
|
||||||
|
|
||||||
// Fetch and save the Style JSONs
|
final lightStyleUrl =
|
||||||
loadStyles();
|
ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl;
|
||||||
|
final darkStyleUrl =
|
||||||
|
ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl;
|
||||||
|
|
||||||
return MapState(
|
return MapState(
|
||||||
themeMode: ThemeMode.values[
|
themeMode: ThemeMode.values[
|
||||||
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
|
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
|
||||||
@@ -34,65 +29,11 @@ class MapStateNotifier extends _$MapStateNotifier {
|
|||||||
appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
|
appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
|
||||||
relativeTime:
|
relativeTime:
|
||||||
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
|
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
|
||||||
|
lightStyleFetched: AsyncData(lightStyleUrl),
|
||||||
|
darkStyleFetched: AsyncData(darkStyleUrl),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadStyles() async {
|
|
||||||
final documents = (await getApplicationDocumentsDirectory()).path;
|
|
||||||
|
|
||||||
// Set to loading
|
|
||||||
state = state.copyWith(lightStyleFetched: const AsyncLoading());
|
|
||||||
|
|
||||||
// Fetch and save light theme
|
|
||||||
final lightResponse = await ref
|
|
||||||
.read(apiServiceProvider)
|
|
||||||
.mapApi
|
|
||||||
.getMapStyleWithHttpInfo(MapTheme.light);
|
|
||||||
|
|
||||||
if (lightResponse.statusCode >= HttpStatus.badRequest) {
|
|
||||||
state = state.copyWith(
|
|
||||||
lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
|
|
||||||
);
|
|
||||||
_log.severe(
|
|
||||||
"Cannot fetch map light style",
|
|
||||||
lightResponse.toLoggerString(),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final lightJSON = lightResponse.body;
|
|
||||||
final lightFile = await File("$documents/map-style-light.json")
|
|
||||||
.writeAsString(lightJSON, flush: true);
|
|
||||||
|
|
||||||
// Update state with path
|
|
||||||
state =
|
|
||||||
state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path));
|
|
||||||
|
|
||||||
// Set to loading
|
|
||||||
state = state.copyWith(darkStyleFetched: const AsyncLoading());
|
|
||||||
|
|
||||||
// Fetch and save dark theme
|
|
||||||
final darkResponse = await ref
|
|
||||||
.read(apiServiceProvider)
|
|
||||||
.mapApi
|
|
||||||
.getMapStyleWithHttpInfo(MapTheme.dark);
|
|
||||||
|
|
||||||
if (darkResponse.statusCode >= HttpStatus.badRequest) {
|
|
||||||
state = state.copyWith(
|
|
||||||
darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
|
|
||||||
);
|
|
||||||
_log.severe("Cannot fetch map dark style", darkResponse.toLoggerString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final darkJSON = darkResponse.body;
|
|
||||||
final darkFile = await File("$documents/map-style-dark.json")
|
|
||||||
.writeAsString(darkJSON, flush: true);
|
|
||||||
|
|
||||||
// Update state with path
|
|
||||||
state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path));
|
|
||||||
}
|
|
||||||
|
|
||||||
void switchTheme(ThemeMode mode) {
|
void switchTheme(ThemeMode mode) {
|
||||||
ref.read(appSettingsServiceProvider).setSetting(
|
ref.read(appSettingsServiceProvider).setSetting(
|
||||||
AppSettingsEnum.mapThemeMode,
|
AppSettingsEnum.mapThemeMode,
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
|||||||
trashDays: 30,
|
trashDays: 30,
|
||||||
oauthButtonText: '',
|
oauthButtonText: '',
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
|
mapLightStyleUrl:
|
||||||
|
'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
),
|
),
|
||||||
serverDiskInfo: const ServerDiskInfo(
|
serverDiskInfo: const ServerDiskInfo(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
final albumRepositoryProvider =
|
||||||
|
Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
|
class AlbumRepository implements IAlbumRepository {
|
||||||
|
final Isar _db;
|
||||||
|
|
||||||
|
AlbumRepository(
|
||||||
|
this._db,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> count({bool? local}) {
|
||||||
|
if (local == true) return _db.albums.where().localIdIsNotNull().count();
|
||||||
|
if (local == false) return _db.albums.where().remoteIdIsNotNull().count();
|
||||||
|
return _db.albums.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> create(Album album) =>
|
||||||
|
_db.writeTxn(() => _db.albums.store(album));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album?> getByName(String name, {bool? shared, bool? remote}) {
|
||||||
|
var query = _db.albums.filter().nameEqualTo(name);
|
||||||
|
if (shared != null) {
|
||||||
|
query = query.sharedEqualTo(shared);
|
||||||
|
}
|
||||||
|
if (remote == true) {
|
||||||
|
query = query.localIdIsNull();
|
||||||
|
} else if (remote == false) {
|
||||||
|
query = query.remoteIdIsNull();
|
||||||
|
}
|
||||||
|
return query.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> update(Album album) =>
|
||||||
|
_db.writeTxn(() => _db.albums.store(album));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(int albumId) =>
|
||||||
|
_db.writeTxn(() => _db.albums.delete(albumId));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Album>> getAll({bool? shared}) {
|
||||||
|
final baseQuery = _db.albums.filter();
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition>? query;
|
||||||
|
if (shared != null) {
|
||||||
|
query = baseQuery.sharedEqualTo(true);
|
||||||
|
}
|
||||||
|
return query?.findAll() ?? _db.albums.where().findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album?> getById(int id) => _db.albums.get(id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeUsers(Album album, List<User> users) =>
|
||||||
|
_db.writeTxn(() => album.sharedUsers.update(unlink: users));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addAssets(Album album, List<Asset> assets) =>
|
||||||
|
_db.writeTxn(() => album.assets.update(link: assets));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeAssets(Album album, List<Asset> assets) =>
|
||||||
|
_db.writeTxn(() => album.assets.update(unlink: assets));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> recalculateMetadata(Album album) async {
|
||||||
|
album.startDate = await album.assets.filter().fileCreatedAtProperty().min();
|
||||||
|
album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
|
||||||
|
album.lastModifiedAssetTimestamp =
|
||||||
|
await album.assets.filter().updatedAtProperty().max();
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/errors.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final albumApiRepositoryProvider = Provider(
|
||||||
|
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||||
|
);
|
||||||
|
|
||||||
|
class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
|
final AlbumsApi _api;
|
||||||
|
|
||||||
|
AlbumApiRepository(this._api);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> get(String id) async {
|
||||||
|
final dto = await _checkNull(_api.getAlbumInfo(id));
|
||||||
|
return _toAlbum(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Album>> getAll({bool? shared}) async {
|
||||||
|
final dtos = await _checkNull(_api.getAllAlbums(shared: shared));
|
||||||
|
return dtos.map(_toAlbum).toList().cast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> create(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
Iterable<String> sharedUserIds = const [],
|
||||||
|
}) async {
|
||||||
|
final users = sharedUserIds.map(
|
||||||
|
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
||||||
|
);
|
||||||
|
final responseDto = await _checkNull(
|
||||||
|
_api.createAlbum(
|
||||||
|
CreateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
assetIds: assetIds.toList(),
|
||||||
|
albumUsers: users.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _toAlbum(responseDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> update(
|
||||||
|
String albumId, {
|
||||||
|
String? name,
|
||||||
|
String? thumbnailAssetId,
|
||||||
|
String? description,
|
||||||
|
bool? activityEnabled,
|
||||||
|
}) async {
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.updateAlbumInfo(
|
||||||
|
albumId,
|
||||||
|
UpdateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
albumThumbnailAssetId: thumbnailAssetId,
|
||||||
|
description: description,
|
||||||
|
isActivityEnabled: activityEnabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _toAlbum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String albumId) {
|
||||||
|
return _api.deleteAlbum(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<({List<String> added, List<String> duplicates})> addAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
) async {
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.addAssetsToAlbum(
|
||||||
|
albumId,
|
||||||
|
BulkIdsDto(ids: assetIds.toList()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<String> added = [];
|
||||||
|
final List<String> duplicates = [];
|
||||||
|
|
||||||
|
for (final result in response) {
|
||||||
|
if (result.success) {
|
||||||
|
added.add(result.id);
|
||||||
|
} else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) {
|
||||||
|
duplicates.add(result.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (added: added, duplicates: duplicates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<({List<String> removed, List<String> failed})> removeAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
) async {
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.removeAssetFromAlbum(
|
||||||
|
albumId,
|
||||||
|
BulkIdsDto(ids: assetIds.toList()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final List<String> removed = [], failed = [];
|
||||||
|
for (final dto in response) {
|
||||||
|
if (dto.success) {
|
||||||
|
removed.add(dto.id);
|
||||||
|
} else {
|
||||||
|
failed.add(dto.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (removed: removed, failed: failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
|
||||||
|
final albumUsers =
|
||||||
|
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.addUsersToAlbum(
|
||||||
|
albumId,
|
||||||
|
AddUsersDto(albumUsers: albumUsers),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _toAlbum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) {
|
||||||
|
return _api.removeUserFromAlbum(albumId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<T> _checkNull<T>(Future<T?> future) async {
|
||||||
|
final response = await future;
|
||||||
|
if (response == null) throw NoResponseDtoError();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Album _toAlbum(AlbumResponseDto dto) {
|
||||||
|
final Album album = Album(
|
||||||
|
remoteId: dto.id,
|
||||||
|
name: dto.albumName,
|
||||||
|
createdAt: dto.createdAt,
|
||||||
|
modifiedAt: dto.updatedAt,
|
||||||
|
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||||
|
shared: dto.shared,
|
||||||
|
startDate: dto.startDate,
|
||||||
|
endDate: dto.endDate,
|
||||||
|
activityEnabled: dto.isActivityEnabled,
|
||||||
|
);
|
||||||
|
album.remoteAssetCount = dto.assetCount;
|
||||||
|
album.owner.value = User.fromSimpleUserDto(dto.owner);
|
||||||
|
album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
|
||||||
|
final users = dto.albumUsers
|
||||||
|
.map((albumUser) => User.fromSimpleUserDto(albumUser.user));
|
||||||
|
album.sharedUsers.addAll(users);
|
||||||
|
final assets = dto.assets.map(Asset.remote).toList();
|
||||||
|
album.assets.addAll(assets);
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||||
|
|
||||||
|
final albumMediaRepositoryProvider = Provider((ref) => AlbumMediaRepository());
|
||||||
|
|
||||||
|
class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||||
|
@override
|
||||||
|
Future<List<Album>> getAll() async {
|
||||||
|
final List<AssetPathEntity> assetPathEntities =
|
||||||
|
await PhotoManager.getAssetPathList(
|
||||||
|
hasAll: true,
|
||||||
|
filterOption: FilterOptionGroup(containsPathModified: true),
|
||||||
|
);
|
||||||
|
return assetPathEntities.map(_toAlbum).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getAssetIds(String albumId) async {
|
||||||
|
final album = await AssetPathEntity.fromId(albumId);
|
||||||
|
final List<AssetEntity> assets =
|
||||||
|
await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
|
||||||
|
return assets.map((e) => e.id).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> getAssetCount(String albumId) async {
|
||||||
|
final album = await AssetPathEntity.fromId(albumId);
|
||||||
|
return album.assetCountAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Asset>> getAssets(
|
||||||
|
String albumId, {
|
||||||
|
int start = 0,
|
||||||
|
int end = 0x7fffffffffffffff,
|
||||||
|
DateTime? modifiedFrom,
|
||||||
|
DateTime? modifiedUntil,
|
||||||
|
bool orderByModificationDate = false,
|
||||||
|
}) async {
|
||||||
|
final onDevice = await AssetPathEntity.fromId(
|
||||||
|
albumId,
|
||||||
|
filterOption: FilterOptionGroup(
|
||||||
|
containsPathModified: true,
|
||||||
|
orders: orderByModificationDate
|
||||||
|
? [const OrderOption(type: OrderOptionType.updateDate)]
|
||||||
|
: [],
|
||||||
|
imageOption: const FilterOption(needTitle: true),
|
||||||
|
videoOption: const FilterOption(needTitle: true),
|
||||||
|
updateTimeCond: modifiedFrom == null && modifiedUntil == null
|
||||||
|
? null
|
||||||
|
: DateTimeCond(
|
||||||
|
min: modifiedFrom ?? DateTime.utc(-271820),
|
||||||
|
max: modifiedUntil ?? DateTime.utc(275760),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<AssetEntity> assets =
|
||||||
|
await onDevice.getAssetListRange(start: start, end: end);
|
||||||
|
return assets.map(AssetMediaRepository.toAsset).toList().cast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> get(
|
||||||
|
String id, {
|
||||||
|
DateTime? modifiedFrom,
|
||||||
|
DateTime? modifiedUntil,
|
||||||
|
}) async {
|
||||||
|
final assetPathEntity = await AssetPathEntity.fromId(id);
|
||||||
|
return _toAlbum(assetPathEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Album _toAlbum(AssetPathEntity assetPathEntity) {
|
||||||
|
final Album album = Album(
|
||||||
|
name: assetPathEntity.name,
|
||||||
|
createdAt:
|
||||||
|
assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
||||||
|
modifiedAt:
|
||||||
|
assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
||||||
|
shared: false,
|
||||||
|
activityEnabled: false,
|
||||||
|
);
|
||||||
|
album.owner.value = Store.get(StoreKey.currentUser);
|
||||||
|
album.localId = assetPathEntity.id;
|
||||||
|
album.isAll = assetPathEntity.isAll;
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
final assetRepositoryProvider =
|
||||||
|
Provider((ref) => AssetRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
|
class AssetRepository implements IAssetRepository {
|
||||||
|
final Isar _db;
|
||||||
|
|
||||||
|
AssetRepository(
|
||||||
|
this._db,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy}) {
|
||||||
|
var query = album.assets.filter();
|
||||||
|
if (notOwnedBy != null) {
|
||||||
|
query = query.not().ownerIdEqualTo(notOwnedBy.isarId);
|
||||||
|
}
|
||||||
|
return query.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteById(List<int> ids) =>
|
||||||
|
_db.writeTxn(() => _db.assets.deleteAll(ids));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Asset?> getByRemoteId(String id) => _db.assets.getByRemoteId(id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
||||||
|
_db.assets.getAllByRemoteId(ids);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||||
|
|
||||||
|
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
|
||||||
|
|
||||||
|
class AssetMediaRepository implements IAssetMediaRepository {
|
||||||
|
@override
|
||||||
|
Future<List<String>> deleteAll(List<String> ids) =>
|
||||||
|
PhotoManager.editor.deleteWithIds(ids);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Asset?> get(String id) async {
|
||||||
|
final entity = await AssetEntity.fromId(id);
|
||||||
|
return toAsset(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Asset? toAsset(AssetEntity? local) {
|
||||||
|
if (local == null) return null;
|
||||||
|
final Asset asset = Asset(
|
||||||
|
checksum: "",
|
||||||
|
localId: local.id,
|
||||||
|
ownerId: Store.get(StoreKey.currentUser).isarId,
|
||||||
|
fileCreatedAt: local.createDateTime,
|
||||||
|
fileModifiedAt: local.modifiedDateTime,
|
||||||
|
updatedAt: local.modifiedDateTime,
|
||||||
|
durationInSeconds: local.duration,
|
||||||
|
type: AssetType.values[local.typeInt],
|
||||||
|
fileName: local.title!,
|
||||||
|
width: local.width,
|
||||||
|
height: local.height,
|
||||||
|
isFavorite: local.isFavorite,
|
||||||
|
);
|
||||||
|
if (asset.fileCreatedAt.year == 1970) {
|
||||||
|
asset.fileCreatedAt = asset.fileModifiedAt;
|
||||||
|
}
|
||||||
|
if (local.latitude != null) {
|
||||||
|
asset.exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
|
||||||
|
}
|
||||||
|
asset.local = local;
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
final backupRepositoryProvider =
|
||||||
|
Provider((ref) => BackupRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
|
class BackupRepository implements IBackupRepository {
|
||||||
|
final Isar _db;
|
||||||
|
|
||||||
|
BackupRepository(
|
||||||
|
this._db,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getIdsBySelection(BackupSelection backup) =>
|
||||||
|
_db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll();
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||||
|
|
||||||
|
final fileMediaRepositoryProvider = Provider((ref) => FileMediaRepository());
|
||||||
|
|
||||||
|
class FileMediaRepository implements IFileMediaRepository {
|
||||||
|
@override
|
||||||
|
Future<Asset?> saveImage(
|
||||||
|
Uint8List data, {
|
||||||
|
required String title,
|
||||||
|
String? relativePath,
|
||||||
|
}) async {
|
||||||
|
final entity = await PhotoManager.editor
|
||||||
|
.saveImage(data, title: title, relativePath: relativePath);
|
||||||
|
return AssetMediaRepository.toAsset(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Asset?> saveLivePhoto({
|
||||||
|
required File image,
|
||||||
|
required File video,
|
||||||
|
required String title,
|
||||||
|
}) async {
|
||||||
|
final entity = await PhotoManager.editor.darwin.saveLivePhoto(
|
||||||
|
imageFile: image,
|
||||||
|
videoFile: video,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
return AssetMediaRepository.toAsset(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Asset?> saveVideo(
|
||||||
|
File file, {
|
||||||
|
required String title,
|
||||||
|
String? relativePath,
|
||||||
|
}) async {
|
||||||
|
final entity = await PhotoManager.editor.saveVideo(
|
||||||
|
file,
|
||||||
|
title: title,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
|
return AssetMediaRepository.toAsset(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearFileCache() => PhotoManager.clearFileCache();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> enableBackgroundAccess() =>
|
||||||
|
PhotoManager.setIgnorePermissionCheck(true);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> requestExtendedPermissions() =>
|
||||||
|
PhotoManager.requestPermissionExtend();
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
final userRepositoryProvider =
|
||||||
|
Provider((ref) => UserRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
|
class UserRepository implements IUserRepository {
|
||||||
|
final Isar _db;
|
||||||
|
|
||||||
|
UserRepository(
|
||||||
|
this._db,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<User>> getByIds(List<String> ids) async =>
|
||||||
|
(await _db.users.getAllById(ids)).cast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<User?> get(String id) => _db.users.getById(id);
|
||||||
|
}
|
||||||
@@ -63,7 +63,6 @@ import 'package:immich_mobile/services/api.service.dart';
|
|||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart' hide LatLng;
|
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class AlbumOptionsRouteArgs {
|
|||||||
class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
|
class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
|
||||||
AlbumPreviewRoute({
|
AlbumPreviewRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required AssetPathEntity album,
|
required Album album,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
AlbumPreviewRoute.name,
|
AlbumPreviewRoute.name,
|
||||||
@@ -218,7 +218,7 @@ class AlbumPreviewRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final AssetPathEntity album;
|
final Album album;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -5,54 +5,64 @@ import 'dart:io';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
|
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/services/user.service.dart';
|
import 'package:immich_mobile/services/user.service.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
final albumServiceProvider = Provider(
|
final albumServiceProvider = Provider(
|
||||||
(ref) => AlbumService(
|
(ref) => AlbumService(
|
||||||
ref.watch(apiServiceProvider),
|
|
||||||
ref.watch(userServiceProvider),
|
ref.watch(userServiceProvider),
|
||||||
ref.watch(syncServiceProvider),
|
ref.watch(syncServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(entityServiceProvider),
|
||||||
|
ref.watch(albumRepositoryProvider),
|
||||||
|
ref.watch(assetRepositoryProvider),
|
||||||
|
ref.watch(backupRepositoryProvider),
|
||||||
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
|
ref.watch(albumApiRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class AlbumService {
|
class AlbumService {
|
||||||
final ApiService _apiService;
|
|
||||||
final UserService _userService;
|
final UserService _userService;
|
||||||
final SyncService _syncService;
|
final SyncService _syncService;
|
||||||
final Isar _db;
|
final EntityService _entityService;
|
||||||
|
final IAlbumRepository _albumRepository;
|
||||||
|
final IAssetRepository _assetRepository;
|
||||||
|
final IBackupRepository _backupAlbumRepository;
|
||||||
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
|
final IAlbumApiRepository _albumApiRepository;
|
||||||
final Logger _log = Logger('AlbumService');
|
final Logger _log = Logger('AlbumService');
|
||||||
Completer<bool> _localCompleter = Completer()..complete(false);
|
Completer<bool> _localCompleter = Completer()..complete(false);
|
||||||
Completer<bool> _remoteCompleter = Completer()..complete(false);
|
Completer<bool> _remoteCompleter = Completer()..complete(false);
|
||||||
|
|
||||||
AlbumService(
|
AlbumService(
|
||||||
this._apiService,
|
|
||||||
this._userService,
|
this._userService,
|
||||||
this._syncService,
|
this._syncService,
|
||||||
this._db,
|
this._entityService,
|
||||||
|
this._albumRepository,
|
||||||
|
this._assetRepository,
|
||||||
|
this._backupAlbumRepository,
|
||||||
|
this._albumMediaRepository,
|
||||||
|
this._albumApiRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
|
||||||
selectedAlbumsQuery() =>
|
|
||||||
_db.backupAlbums.filter().selectionEqualTo(BackupSelection.select);
|
|
||||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
|
||||||
excludedAlbumsQuery() =>
|
|
||||||
_db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
|
|
||||||
|
|
||||||
/// Checks all selected device albums for changes of albums and their assets
|
/// Checks all selected device albums for changes of albums and their assets
|
||||||
/// Updates the local database and returns `true` if there were any changes
|
/// Updates the local database and returns `true` if there were any changes
|
||||||
Future<bool> refreshDeviceAlbums() async {
|
Future<bool> refreshDeviceAlbums() async {
|
||||||
@@ -65,22 +75,18 @@ class AlbumService {
|
|||||||
final Stopwatch sw = Stopwatch()..start();
|
final Stopwatch sw = Stopwatch()..start();
|
||||||
bool changes = false;
|
bool changes = false;
|
||||||
try {
|
try {
|
||||||
final List<String> excludedIds =
|
final List<String> excludedIds = await _backupAlbumRepository
|
||||||
await excludedAlbumsQuery().idProperty().findAll();
|
.getIdsBySelection(BackupSelection.exclude);
|
||||||
final List<String> selectedIds =
|
final List<String> selectedIds = await _backupAlbumRepository
|
||||||
await selectedAlbumsQuery().idProperty().findAll();
|
.getIdsBySelection(BackupSelection.select);
|
||||||
if (selectedIds.isEmpty) {
|
if (selectedIds.isEmpty) {
|
||||||
final numLocal = await _db.albums.where().localIdIsNotNull().count();
|
final numLocal = await _albumRepository.count(local: true);
|
||||||
if (numLocal > 0) {
|
if (numLocal > 0) {
|
||||||
_syncService.removeAllLocalAlbumsAndAssets();
|
_syncService.removeAllLocalAlbumsAndAssets();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final List<AssetPathEntity> onDevice =
|
final List<Album> onDevice = await _albumMediaRepository.getAll();
|
||||||
await PhotoManager.getAssetPathList(
|
|
||||||
hasAll: true,
|
|
||||||
filterOption: FilterOptionGroup(containsPathModified: true),
|
|
||||||
);
|
|
||||||
_log.info("Found ${onDevice.length} device albums");
|
_log.info("Found ${onDevice.length} device albums");
|
||||||
Set<String>? excludedAssets;
|
Set<String>? excludedAssets;
|
||||||
if (excludedIds.isNotEmpty) {
|
if (excludedIds.isNotEmpty) {
|
||||||
@@ -96,13 +102,15 @@ class AlbumService {
|
|||||||
_log.info("Found ${excludedAssets.length} assets to exclude");
|
_log.info("Found ${excludedAssets.length} assets to exclude");
|
||||||
}
|
}
|
||||||
// remove all excluded albums
|
// remove all excluded albums
|
||||||
onDevice.removeWhere((e) => excludedIds.contains(e.id));
|
onDevice.removeWhere((e) => excludedIds.contains(e.localId));
|
||||||
_log.info(
|
_log.info(
|
||||||
"Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
|
"Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final hasAll = selectedIds
|
final hasAll = selectedIds
|
||||||
.map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
|
.map(
|
||||||
|
(id) => onDevice.firstWhereOrNull((album) => album.localId == id),
|
||||||
|
)
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
.any((a) => a.isAll);
|
.any((a) => a.isAll);
|
||||||
if (hasAll) {
|
if (hasAll) {
|
||||||
@@ -114,7 +122,7 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// keep only the explicitly selected albums
|
// keep only the explicitly selected albums
|
||||||
onDevice.removeWhere((e) => !selectedIds.contains(e.id));
|
onDevice.removeWhere((e) => !selectedIds.contains(e.localId));
|
||||||
_log.info("'Recents' is not selected, keeping only selected albums");
|
_log.info("'Recents' is not selected, keeping only selected albums");
|
||||||
}
|
}
|
||||||
changes =
|
changes =
|
||||||
@@ -128,15 +136,15 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Set<String>> _loadExcludedAssetIds(
|
Future<Set<String>> _loadExcludedAssetIds(
|
||||||
List<AssetPathEntity> albums,
|
List<Album> albums,
|
||||||
List<String> excludedAlbumIds,
|
List<String> excludedAlbumIds,
|
||||||
) async {
|
) async {
|
||||||
final Set<String> result = HashSet<String>();
|
final Set<String> result = HashSet<String>();
|
||||||
for (AssetPathEntity a in albums) {
|
for (Album album in albums) {
|
||||||
if (excludedAlbumIds.contains(a.id)) {
|
if (excludedAlbumIds.contains(album.localId)) {
|
||||||
final List<AssetEntity> assets =
|
final assetIds =
|
||||||
await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
|
await _albumMediaRepository.getAssetIds(album.localId!);
|
||||||
result.addAll(assets.map((e) => e.id));
|
result.addAll(assetIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -154,17 +162,11 @@ class AlbumService {
|
|||||||
bool changes = false;
|
bool changes = false;
|
||||||
try {
|
try {
|
||||||
await _userService.refreshUsers();
|
await _userService.refreshUsers();
|
||||||
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
|
final List<Album> serverAlbums =
|
||||||
.getAllAlbums(shared: isShared ? true : null);
|
await _albumApiRepository.getAll(shared: isShared ? true : null);
|
||||||
if (serverAlbums == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
changes = await _syncService.syncRemoteAlbumsToDb(
|
changes = await _syncService.syncRemoteAlbumsToDb(
|
||||||
serverAlbums,
|
serverAlbums,
|
||||||
isShared: isShared,
|
isShared: isShared,
|
||||||
loadDetails: (dto) async => dto.assetCount == dto.assets.length
|
|
||||||
? dto
|
|
||||||
: (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
|
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
_remoteCompleter.complete(changes);
|
_remoteCompleter.complete(changes);
|
||||||
@@ -178,30 +180,13 @@ class AlbumService {
|
|||||||
Iterable<Asset> assets, [
|
Iterable<Asset> assets, [
|
||||||
Iterable<User> sharedUsers = const [],
|
Iterable<User> sharedUsers = const [],
|
||||||
]) async {
|
]) async {
|
||||||
try {
|
final Album album = await _albumApiRepository.create(
|
||||||
AlbumResponseDto? remote = await _apiService.albumsApi.createAlbum(
|
albumName,
|
||||||
CreateAlbumDto(
|
assetIds: assets.map((asset) => asset.remoteId!),
|
||||||
albumName: albumName,
|
sharedUserIds: sharedUsers.map((user) => user.id),
|
||||||
assetIds: assets.map((asset) => asset.remoteId!).toList(),
|
);
|
||||||
albumUsers: sharedUsers
|
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||||
.map(
|
return _albumRepository.create(album);
|
||||||
(e) => AlbumUserCreateDto(
|
|
||||||
userId: e.id,
|
|
||||||
role: AlbumUserRole.editor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (remote != null) {
|
|
||||||
Album album = await Album.remote(remote);
|
|
||||||
await _db.writeTxn(() => _db.albums.store(album));
|
|
||||||
return album;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error createSharedAlbum ${e.toString()}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -212,8 +197,7 @@ class AlbumService {
|
|||||||
for (int round = 0;; round++) {
|
for (int round = 0;; round++) {
|
||||||
final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
|
final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
|
||||||
|
|
||||||
if (null ==
|
if (null == await _albumRepository.getByName(proposedName)) {
|
||||||
await _db.albums.filter().nameEqualTo(proposedName).findFirst()) {
|
|
||||||
return proposedName;
|
return proposedName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,32 +218,21 @@ class AlbumService {
|
|||||||
Album album,
|
Album album,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
var response = await _apiService.albumsApi.addAssetsToAlbum(
|
final result = await _albumApiRepository.addAssets(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
|
assets.map((asset) => asset.remoteId!),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null) {
|
final List<Asset> addedAssets = result.added
|
||||||
List<Asset> successAssets = [];
|
.map((id) => assets.firstWhere((asset) => asset.remoteId == id))
|
||||||
List<String> duplicatedAssets = [];
|
.toList();
|
||||||
|
|
||||||
for (final result in response) {
|
await _updateAssets(album.id, add: addedAssets);
|
||||||
if (result.success) {
|
|
||||||
successAssets
|
|
||||||
.add(assets.firstWhere((asset) => asset.remoteId == result.id));
|
|
||||||
} else if (!result.success &&
|
|
||||||
result.error == BulkIdResponseDtoErrorEnum.duplicate) {
|
|
||||||
duplicatedAssets.add(result.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _updateAssets(album.id, add: successAssets);
|
return AlbumAddAssetsResponse(
|
||||||
|
alreadyInAlbum: result.duplicates,
|
||||||
return AlbumAddAssetsResponse(
|
successfullyAdded: addedAssets.length,
|
||||||
alreadyInAlbum: duplicatedAssets,
|
);
|
||||||
successfullyAdded: successAssets.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||||
}
|
}
|
||||||
@@ -268,20 +241,15 @@ class AlbumService {
|
|||||||
|
|
||||||
Future<void> _updateAssets(
|
Future<void> _updateAssets(
|
||||||
int albumId, {
|
int albumId, {
|
||||||
Iterable<Asset> add = const [],
|
List<Asset> add = const [],
|
||||||
Iterable<Asset> remove = const [],
|
List<Asset> remove = const [],
|
||||||
}) {
|
}) async {
|
||||||
return _db.writeTxn(() async {
|
final album = await _albumRepository.getById(albumId);
|
||||||
final album = await _db.albums.get(albumId);
|
if (album == null) return;
|
||||||
if (album == null) return;
|
await _albumRepository.addAssets(album, add);
|
||||||
await album.assets.update(link: add, unlink: remove);
|
await _albumRepository.removeAssets(album, remove);
|
||||||
album.startDate =
|
await _albumRepository.recalculateMetadata(album);
|
||||||
await album.assets.filter().fileCreatedAtProperty().min();
|
await _albumRepository.update(album);
|
||||||
album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
|
|
||||||
album.lastModifiedAssetTimestamp =
|
|
||||||
await album.assets.filter().updatedAtProperty().max();
|
|
||||||
await _db.albums.put(album);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalUserToAlbum(
|
Future<bool> addAdditionalUserToAlbum(
|
||||||
@@ -289,24 +257,11 @@ class AlbumService {
|
|||||||
Album album,
|
Album album,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final List<AlbumUserAddDto> albumUsers = sharedUserIds
|
final updatedAlbum =
|
||||||
.map((userId) => AlbumUserAddDto(userId: userId))
|
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
|
||||||
.toList();
|
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||||
|
await _albumRepository.update(updatedAlbum);
|
||||||
final result = await _apiService.albumsApi.addUsersToAlbum(
|
return true;
|
||||||
album.remoteId!,
|
|
||||||
AddUsersDto(albumUsers: albumUsers),
|
|
||||||
);
|
|
||||||
if (result != null) {
|
|
||||||
album.sharedUsers
|
|
||||||
.addAll((await _db.users.getAllById(sharedUserIds)).cast());
|
|
||||||
album.shared = result.shared;
|
|
||||||
await _db.writeTxn(() async {
|
|
||||||
await _db.albums.put(album);
|
|
||||||
await album.sharedUsers.save();
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
||||||
}
|
}
|
||||||
@@ -315,15 +270,13 @@ class AlbumService {
|
|||||||
|
|
||||||
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
||||||
try {
|
try {
|
||||||
final result = await _apiService.albumsApi.updateAlbumInfo(
|
final updatedAlbum = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
UpdateAlbumDto(isActivityEnabled: enabled),
|
activityEnabled: enabled,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||||
album.activityEnabled = enabled;
|
await _albumRepository.update(updatedAlbum);
|
||||||
await _db.writeTxn(() => _db.albums.put(album));
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error setActivityEnabled ${e.toString()}");
|
debugPrint("Error setActivityEnabled ${e.toString()}");
|
||||||
}
|
}
|
||||||
@@ -332,29 +285,29 @@ class AlbumService {
|
|||||||
|
|
||||||
Future<bool> deleteAlbum(Album album) async {
|
Future<bool> deleteAlbum(Album album) async {
|
||||||
try {
|
try {
|
||||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
final user = Store.get(StoreKey.currentUser);
|
||||||
if (album.owner.value?.isarId == userId) {
|
if (album.owner.value?.isarId == user.isarId) {
|
||||||
await _apiService.albumsApi.deleteAlbum(album.remoteId!);
|
await _albumApiRepository.delete(album.remoteId!);
|
||||||
}
|
}
|
||||||
if (album.shared) {
|
if (album.shared) {
|
||||||
final foreignAssets =
|
final foreignAssets =
|
||||||
await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
|
await _assetRepository.getByAlbum(album, notOwnedBy: user);
|
||||||
await _db.writeTxn(() => _db.albums.delete(album.id));
|
await _albumRepository.delete(album.id);
|
||||||
final List<Album> albums =
|
|
||||||
await _db.albums.filter().sharedEqualTo(true).findAll();
|
final List<Album> albums = await _albumRepository.getAll(shared: true);
|
||||||
final List<Asset> existing = [];
|
final List<Asset> existing = [];
|
||||||
for (Album a in albums) {
|
for (Album album in albums) {
|
||||||
existing.addAll(
|
existing.addAll(
|
||||||
await a.assets.filter().not().ownerIdEqualTo(userId).findAll(),
|
await _assetRepository.getByAlbum(album, notOwnedBy: user),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final List<int> idsToRemove =
|
final List<int> idsToRemove =
|
||||||
_syncService.sharedAssetsToRemove(foreignAssets, existing);
|
_syncService.sharedAssetsToRemove(foreignAssets, existing);
|
||||||
if (idsToRemove.isNotEmpty) {
|
if (idsToRemove.isNotEmpty) {
|
||||||
await _db.writeTxn(() => _db.assets.deleteAll(idsToRemove));
|
await _assetRepository.deleteById(idsToRemove);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await _db.writeTxn(() => _db.albums.delete(album.id));
|
await _albumRepository.delete(album.id);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -365,7 +318,7 @@ class AlbumService {
|
|||||||
|
|
||||||
Future<bool> leaveAlbum(Album album) async {
|
Future<bool> leaveAlbum(Album album) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
|
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error leaveAlbum ${e.toString()}");
|
debugPrint("Error leaveAlbum ${e.toString()}");
|
||||||
@@ -378,21 +331,14 @@ class AlbumService {
|
|||||||
Iterable<Asset> assets,
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final response = await _apiService.albumsApi.removeAssetFromAlbum(
|
final result = await _albumApiRepository.removeAssets(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
BulkIdsDto(
|
assets.map((asset) => asset.remoteId!),
|
||||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (response != null) {
|
final toRemove = result.removed
|
||||||
final toRemove = response.every((e) => e.success)
|
.map((id) => assets.firstWhere((asset) => asset.remoteId == id));
|
||||||
? assets
|
await _updateAssets(album.id, remove: toRemove.toList());
|
||||||
: response
|
return true;
|
||||||
.where((e) => e.success)
|
|
||||||
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
|
|
||||||
await _updateAssets(album.id, remove: toRemove);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
||||||
}
|
}
|
||||||
@@ -404,18 +350,16 @@ class AlbumService {
|
|||||||
User user,
|
User user,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.albumsApi.removeUserFromAlbum(
|
await _albumApiRepository.removeUser(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
user.id,
|
userId: user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
album.sharedUsers.remove(user);
|
album.sharedUsers.remove(user);
|
||||||
await _db.writeTxn(() async {
|
await _albumRepository.removeUsers(album, [user]);
|
||||||
await album.sharedUsers.update(unlink: [user]);
|
final a = await _albumRepository.getById(album.id);
|
||||||
final a = await _db.albums.get(album.id);
|
// trigger watcher
|
||||||
// trigger watcher
|
await _albumRepository.update(a!);
|
||||||
await _db.albums.put(a!);
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -429,15 +373,12 @@ class AlbumService {
|
|||||||
String newAlbumTitle,
|
String newAlbumTitle,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.albumsApi.updateAlbumInfo(
|
album = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
UpdateAlbumDto(
|
name: newAlbumTitle,
|
||||||
albumName: newAlbumTitle,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
album.name = newAlbumTitle;
|
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||||
await _db.writeTxn(() => _db.albums.put(album));
|
await _albumRepository.update(album);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
||||||
@@ -445,14 +386,8 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Album?> getAlbumByName(String name, bool remoteOnly) async {
|
Future<Album?> getAlbumByName(String name, bool remoteOnly) =>
|
||||||
return _db.albums
|
_albumRepository.getByName(name, remote: remoteOnly ? true : null);
|
||||||
.filter()
|
|
||||||
.optional(remoteOnly, (q) => q.localIdIsNull())
|
|
||||||
.nameEqualTo(name)
|
|
||||||
.sharedEqualTo(false)
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Add the uploaded asset to the selected albums
|
/// Add the uploaded asset to the selected albums
|
||||||
@@ -464,12 +399,8 @@ class AlbumService {
|
|||||||
for (final albumName in albumNames) {
|
for (final albumName in albumNames) {
|
||||||
Album? album = await getAlbumByName(albumName, true);
|
Album? album = await getAlbumByName(albumName, true);
|
||||||
album ??= await createAlbum(albumName, []);
|
album ??= await createAlbum(albumName, []);
|
||||||
|
|
||||||
if (album != null && album.remoteId != null) {
|
if (album != null && album.remoteId != null) {
|
||||||
await _apiService.albumsApi.addAssetsToAlbum(
|
await _albumApiRepository.addAssets(album.remoteId!, assetIds);
|
||||||
album.remoteId!,
|
|
||||||
BulkIdsDto(ids: assetIds),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ class AssetService {
|
|||||||
|
|
||||||
for (BackupCandidate candidate in candidates) {
|
for (BackupCandidate candidate in candidates) {
|
||||||
final asset = remoteAssets.firstWhereOrNull(
|
final asset = remoteAssets.firstWhereOrNull(
|
||||||
(a) => a.localId == candidate.asset.id,
|
(a) => a.localId == candidate.asset.localId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (asset != null) {
|
if (asset != null) {
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/main.dart';
|
import 'package:immich_mobile/main.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/services/localization.service.dart';
|
import 'package:immich_mobile/services/localization.service.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
@@ -30,7 +38,7 @@ import 'package:immich_mobile/utils/diff.dart';
|
|||||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||||
|
|
||||||
final backgroundServiceProvider = Provider(
|
final backgroundServiceProvider = Provider(
|
||||||
(ref) => BackgroundService(),
|
(ref) => BackgroundService(),
|
||||||
@@ -355,14 +363,44 @@ class BackgroundService {
|
|||||||
AppSettingsService settingService = AppSettingsService();
|
AppSettingsService settingService = AppSettingsService();
|
||||||
AppSettingsService settingsService = AppSettingsService();
|
AppSettingsService settingsService = AppSettingsService();
|
||||||
PartnerService partnerService = PartnerService(apiService, db);
|
PartnerService partnerService = PartnerService(apiService, db);
|
||||||
HashService hashService = HashService(db, this);
|
AlbumRepository albumRepository = AlbumRepository(db);
|
||||||
SyncService syncSerive = SyncService(db, hashService);
|
AssetRepository assetRepository = AssetRepository(db);
|
||||||
|
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||||
|
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||||
|
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||||
|
UserRepository userRepository = UserRepository(db);
|
||||||
|
AlbumApiRepository albumApiRepository =
|
||||||
|
AlbumApiRepository(apiService.albumsApi);
|
||||||
|
HashService hashService = HashService(db, this, albumMediaRepository);
|
||||||
|
EntityService entityService =
|
||||||
|
EntityService(assetRepository, userRepository);
|
||||||
|
SyncService syncSerive = SyncService(
|
||||||
|
db,
|
||||||
|
hashService,
|
||||||
|
entityService,
|
||||||
|
albumMediaRepository,
|
||||||
|
albumApiRepository,
|
||||||
|
);
|
||||||
UserService userService =
|
UserService userService =
|
||||||
UserService(apiService, db, syncSerive, partnerService);
|
UserService(apiService, db, syncSerive, partnerService);
|
||||||
AlbumService albumService =
|
AlbumService albumService = AlbumService(
|
||||||
AlbumService(apiService, userService, syncSerive, db);
|
userService,
|
||||||
BackupService backupService =
|
syncSerive,
|
||||||
BackupService(apiService, db, settingService, albumService);
|
entityService,
|
||||||
|
albumRepository,
|
||||||
|
assetRepository,
|
||||||
|
backupAlbumRepository,
|
||||||
|
albumMediaRepository,
|
||||||
|
albumApiRepository,
|
||||||
|
);
|
||||||
|
BackupService backupService = BackupService(
|
||||||
|
apiService,
|
||||||
|
db,
|
||||||
|
settingService,
|
||||||
|
albumService,
|
||||||
|
albumMediaRepository,
|
||||||
|
fileMediaRepository,
|
||||||
|
);
|
||||||
|
|
||||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||||
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
||||||
@@ -370,7 +408,7 @@ class BackgroundService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await PhotoManager.setIgnorePermissionCheck(true);
|
await fileMediaRepository.enableBackgroundAccess();
|
||||||
|
|
||||||
do {
|
do {
|
||||||
final bool backupOk = await _runBackup(
|
final bool backupOk = await _runBackup(
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ import 'package:cancellation_token_http/http.dart' as http;
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.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';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||||
@@ -16,6 +20,8 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
|||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
@@ -24,7 +30,7 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:permission_handler/permission_handler.dart' as pm;
|
import 'package:permission_handler/permission_handler.dart' as pm;
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||||
|
|
||||||
final backupServiceProvider = Provider(
|
final backupServiceProvider = Provider(
|
||||||
(ref) => BackupService(
|
(ref) => BackupService(
|
||||||
@@ -32,6 +38,8 @@ final backupServiceProvider = Provider(
|
|||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
ref.watch(appSettingsServiceProvider),
|
ref.watch(appSettingsServiceProvider),
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -42,12 +50,16 @@ class BackupService {
|
|||||||
final Logger _log = Logger("BackupService");
|
final Logger _log = Logger("BackupService");
|
||||||
final AppSettingsService _appSetting;
|
final AppSettingsService _appSetting;
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
|
|
||||||
BackupService(
|
BackupService(
|
||||||
this._apiService,
|
this._apiService,
|
||||||
this._db,
|
this._db,
|
||||||
this._appSetting,
|
this._appSetting,
|
||||||
this._albumService,
|
this._albumService,
|
||||||
|
this._albumMediaRepository,
|
||||||
|
this._fileMediaRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<String>?> getDeviceBackupAsset() async {
|
Future<List<String>?> getDeviceBackupAsset() async {
|
||||||
@@ -86,44 +98,17 @@ class BackupService {
|
|||||||
List<BackupAlbum> excludedBackupAlbums, {
|
List<BackupAlbum> excludedBackupAlbums, {
|
||||||
bool useTimeFilter = true,
|
bool useTimeFilter = true,
|
||||||
}) async {
|
}) async {
|
||||||
final filter = FilterOptionGroup(
|
|
||||||
containsPathModified: true,
|
|
||||||
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
|
||||||
// title is needed to create Assets
|
|
||||||
imageOption: const FilterOption(needTitle: true),
|
|
||||||
videoOption: const FilterOption(needTitle: true),
|
|
||||||
);
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
final List<AssetPathEntity?> selectedAlbums =
|
|
||||||
await _loadAlbumsWithTimeFilter(
|
|
||||||
selectedBackupAlbums,
|
|
||||||
filter,
|
|
||||||
now,
|
|
||||||
useTimeFilter: useTimeFilter,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedAlbums.every((e) => e == null)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<AssetPathEntity?> excludedAlbums =
|
|
||||||
await _loadAlbumsWithTimeFilter(
|
|
||||||
excludedBackupAlbums,
|
|
||||||
filter,
|
|
||||||
now,
|
|
||||||
useTimeFilter: useTimeFilter,
|
|
||||||
);
|
|
||||||
|
|
||||||
final Set<BackupCandidate> toAdd = await _fetchAssetsAndUpdateLastBackup(
|
final Set<BackupCandidate> toAdd = await _fetchAssetsAndUpdateLastBackup(
|
||||||
selectedAlbums,
|
|
||||||
selectedBackupAlbums,
|
selectedBackupAlbums,
|
||||||
now,
|
now,
|
||||||
useTimeFilter: useTimeFilter,
|
useTimeFilter: useTimeFilter,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (toAdd.isEmpty) return {};
|
||||||
|
|
||||||
final Set<BackupCandidate> toRemove = await _fetchAssetsAndUpdateLastBackup(
|
final Set<BackupCandidate> toRemove = await _fetchAssetsAndUpdateLastBackup(
|
||||||
excludedAlbums,
|
|
||||||
excludedBackupAlbums,
|
excludedBackupAlbums,
|
||||||
now,
|
now,
|
||||||
useTimeFilter: useTimeFilter,
|
useTimeFilter: useTimeFilter,
|
||||||
@@ -132,92 +117,62 @@ class BackupService {
|
|||||||
return toAdd.difference(toRemove);
|
return toAdd.difference(toRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter(
|
|
||||||
List<BackupAlbum> albums,
|
|
||||||
FilterOptionGroup filter,
|
|
||||||
DateTime now, {
|
|
||||||
bool useTimeFilter = true,
|
|
||||||
}) async {
|
|
||||||
List<AssetPathEntity?> result = [];
|
|
||||||
for (BackupAlbum backupAlbum in albums) {
|
|
||||||
try {
|
|
||||||
final optionGroup = useTimeFilter
|
|
||||||
? filter.copyWith(
|
|
||||||
updateTimeCond: DateTimeCond(
|
|
||||||
// subtract 2 seconds to prevent missing assets due to rounding issues
|
|
||||||
min: backupAlbum.lastBackup
|
|
||||||
.subtract(const Duration(seconds: 2)),
|
|
||||||
max: now,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: filter;
|
|
||||||
|
|
||||||
final AssetPathEntity album =
|
|
||||||
await AssetPathEntity.obtainPathFromProperties(
|
|
||||||
id: backupAlbum.id,
|
|
||||||
optionGroup: optionGroup,
|
|
||||||
maxDateTimeToNow: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
result.add(album);
|
|
||||||
} on StateError {
|
|
||||||
// either there are no assets matching the filter criteria OR the album no longer exists
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Set<BackupCandidate>> _fetchAssetsAndUpdateLastBackup(
|
Future<Set<BackupCandidate>> _fetchAssetsAndUpdateLastBackup(
|
||||||
List<AssetPathEntity?> localAlbums,
|
|
||||||
List<BackupAlbum> backupAlbums,
|
List<BackupAlbum> backupAlbums,
|
||||||
DateTime now, {
|
DateTime now, {
|
||||||
bool useTimeFilter = true,
|
bool useTimeFilter = true,
|
||||||
}) async {
|
}) async {
|
||||||
Set<BackupCandidate> candidate = {};
|
Set<BackupCandidate> candidates = {};
|
||||||
|
|
||||||
for (int i = 0; i < localAlbums.length; i++) {
|
for (final BackupAlbum backupAlbum in backupAlbums) {
|
||||||
final localAlbum = localAlbums[i];
|
final Album localAlbum;
|
||||||
if (localAlbum == null) {
|
try {
|
||||||
|
localAlbum = await _albumMediaRepository.get(backupAlbum.id);
|
||||||
|
} on StateError {
|
||||||
|
// the album no longer exists
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useTimeFilter &&
|
if (useTimeFilter &&
|
||||||
localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) ==
|
localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) {
|
||||||
true) {
|
continue;
|
||||||
|
}
|
||||||
|
final List<Asset> assets;
|
||||||
|
try {
|
||||||
|
assets = await _albumMediaRepository.getAssets(
|
||||||
|
backupAlbum.id,
|
||||||
|
modifiedFrom: useTimeFilter
|
||||||
|
?
|
||||||
|
// subtract 2 seconds to prevent missing assets due to rounding issues
|
||||||
|
backupAlbum.lastBackup.subtract(const Duration(seconds: 2))
|
||||||
|
: null,
|
||||||
|
modifiedUntil: useTimeFilter ? now : null,
|
||||||
|
);
|
||||||
|
} on StateError {
|
||||||
|
// either there are no assets matching the filter criteria OR the album no longer exists
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = await localAlbum.getAssetListRange(
|
|
||||||
start: 0,
|
|
||||||
end: await localAlbum.assetCountAsync,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add album's name to the asset info
|
// Add album's name to the asset info
|
||||||
for (final asset in assets) {
|
for (final asset in assets) {
|
||||||
List<String> albumNames = [localAlbum.name];
|
List<String> albumNames = [localAlbum.name];
|
||||||
|
|
||||||
final existingAsset = candidate.firstWhereOrNull(
|
final existingAsset = candidates.firstWhereOrNull(
|
||||||
(a) => a.asset.id == asset.id,
|
(candidate) => candidate.asset.localId == asset.localId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingAsset != null) {
|
if (existingAsset != null) {
|
||||||
albumNames.addAll(existingAsset.albumNames);
|
albumNames.addAll(existingAsset.albumNames);
|
||||||
candidate.remove(existingAsset);
|
candidates.remove(existingAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate.add(
|
candidates.add(BackupCandidate(asset: asset, albumNames: albumNames));
|
||||||
BackupCandidate(
|
|
||||||
asset: asset,
|
|
||||||
albumNames: albumNames,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
backupAlbums[i].lastBackup = now;
|
backupAlbum.lastBackup = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidate;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new list of assets not yet uploaded
|
/// Returns a new list of assets not yet uploaded
|
||||||
@@ -230,7 +185,7 @@ class BackupService {
|
|||||||
|
|
||||||
final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
|
final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
|
||||||
candidates.removeWhere(
|
candidates.removeWhere(
|
||||||
(candidate) => duplicatedAssetIds.contains(candidate.asset.id),
|
(candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (candidates.isEmpty) {
|
if (candidates.isEmpty) {
|
||||||
@@ -243,7 +198,7 @@ class BackupService {
|
|||||||
final CheckExistingAssetsResponseDto? duplicates =
|
final CheckExistingAssetsResponseDto? duplicates =
|
||||||
await _apiService.assetsApi.checkExistingAssets(
|
await _apiService.assetsApi.checkExistingAssets(
|
||||||
CheckExistingAssetsDto(
|
CheckExistingAssetsDto(
|
||||||
deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
|
deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(),
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -259,7 +214,7 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existing.isNotEmpty) {
|
if (existing.isNotEmpty) {
|
||||||
candidates.removeWhere((c) => existing.contains(c.asset.id));
|
candidates.removeWhere((c) => existing.contains(c.asset.localId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidates;
|
return candidates;
|
||||||
@@ -278,7 +233,7 @@ class BackupService {
|
|||||||
|
|
||||||
// DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
|
// DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
await PhotoManager.requestPermissionExtend();
|
await _fileMediaRepository.requestExtendedPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -289,9 +244,9 @@ class BackupService {
|
|||||||
List<BackupCandidate> _sortPhotosFirst(List<BackupCandidate> candidates) {
|
List<BackupCandidate> _sortPhotosFirst(List<BackupCandidate> candidates) {
|
||||||
return candidates.sorted(
|
return candidates.sorted(
|
||||||
(a, b) {
|
(a, b) {
|
||||||
final cmp = a.asset.typeInt - b.asset.typeInt;
|
final cmp = a.asset.type.index - b.asset.type.index;
|
||||||
if (cmp != 0) return cmp;
|
if (cmp != 0) return cmp;
|
||||||
return a.asset.createDateTime.compareTo(b.asset.createDateTime);
|
return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -325,13 +280,13 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (final candidate in candidates) {
|
for (final candidate in candidates) {
|
||||||
final AssetEntity entity = candidate.asset;
|
final Asset asset = candidate.asset;
|
||||||
File? file;
|
File? file;
|
||||||
File? livePhotoFile;
|
File? livePhotoFile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final isAvailableLocally =
|
final isAvailableLocally =
|
||||||
await entity.isLocallyAvailable(isOrigin: true);
|
await asset.local!.isLocallyAvailable(isOrigin: true);
|
||||||
|
|
||||||
// Handle getting files from iCloud
|
// Handle getting files from iCloud
|
||||||
if (!isAvailableLocally && Platform.isIOS) {
|
if (!isAvailableLocally && Platform.isIOS) {
|
||||||
@@ -342,39 +297,41 @@ class BackupService {
|
|||||||
|
|
||||||
onCurrentAsset(
|
onCurrentAsset(
|
||||||
CurrentUploadAsset(
|
CurrentUploadAsset(
|
||||||
id: entity.id,
|
id: asset.localId!,
|
||||||
fileCreatedAt: entity.createDateTime.year == 1970
|
fileCreatedAt: asset.fileCreatedAt.year == 1970
|
||||||
? entity.modifiedDateTime
|
? asset.fileModifiedAt
|
||||||
: entity.createDateTime,
|
: asset.fileCreatedAt,
|
||||||
fileName: await entity.titleAsync,
|
fileName: asset.fileName,
|
||||||
fileType: _getAssetType(entity.type),
|
fileType: _getAssetType(asset.type),
|
||||||
iCloudAsset: true,
|
iCloudAsset: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
file = await entity.loadFile(progressHandler: pmProgressHandler);
|
file =
|
||||||
if (entity.isLivePhoto) {
|
await asset.local!.loadFile(progressHandler: pmProgressHandler);
|
||||||
livePhotoFile = await entity.loadFile(
|
if (asset.local!.isLivePhoto) {
|
||||||
|
livePhotoFile = await asset.local!.loadFile(
|
||||||
withSubtype: true,
|
withSubtype: true,
|
||||||
progressHandler: pmProgressHandler,
|
progressHandler: pmProgressHandler,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (entity.type == AssetType.video) {
|
if (asset.type == AssetType.video) {
|
||||||
file = await entity.originFile;
|
file = await asset.local!.originFile;
|
||||||
} else {
|
} else {
|
||||||
file = await entity.originFile.timeout(const Duration(seconds: 5));
|
file = await asset.local!.originFile
|
||||||
if (entity.isLivePhoto) {
|
.timeout(const Duration(seconds: 5));
|
||||||
livePhotoFile = await entity.originFileWithSubtype
|
if (asset.local!.isLivePhoto) {
|
||||||
|
livePhotoFile = await asset.local!.originFileWithSubtype
|
||||||
.timeout(const Duration(seconds: 5));
|
.timeout(const Duration(seconds: 5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
String originalFileName = await entity.titleAsync;
|
String originalFileName = asset.fileName;
|
||||||
|
|
||||||
if (entity.isLivePhoto) {
|
if (asset.local!.isLivePhoto) {
|
||||||
if (livePhotoFile == null) {
|
if (livePhotoFile == null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"Failed to obtain motion part of the livePhoto - $originalFileName",
|
"Failed to obtain motion part of the livePhoto - $originalFileName",
|
||||||
@@ -398,31 +355,31 @@ class BackupService {
|
|||||||
|
|
||||||
baseRequest.headers.addAll(ApiService.getRequestHeaders());
|
baseRequest.headers.addAll(ApiService.getRequestHeaders());
|
||||||
baseRequest.headers["Transfer-Encoding"] = "chunked";
|
baseRequest.headers["Transfer-Encoding"] = "chunked";
|
||||||
baseRequest.fields['deviceAssetId'] = entity.id;
|
baseRequest.fields['deviceAssetId'] = asset.localId!;
|
||||||
baseRequest.fields['deviceId'] = deviceId;
|
baseRequest.fields['deviceId'] = deviceId;
|
||||||
baseRequest.fields['fileCreatedAt'] =
|
baseRequest.fields['fileCreatedAt'] =
|
||||||
entity.createDateTime.toUtc().toIso8601String();
|
asset.fileCreatedAt.toUtc().toIso8601String();
|
||||||
baseRequest.fields['fileModifiedAt'] =
|
baseRequest.fields['fileModifiedAt'] =
|
||||||
entity.modifiedDateTime.toUtc().toIso8601String();
|
asset.fileModifiedAt.toUtc().toIso8601String();
|
||||||
baseRequest.fields['isFavorite'] = entity.isFavorite.toString();
|
baseRequest.fields['isFavorite'] = asset.isFavorite.toString();
|
||||||
baseRequest.fields['duration'] = entity.videoDuration.toString();
|
baseRequest.fields['duration'] = asset.duration.toString();
|
||||||
baseRequest.files.add(assetRawUploadData);
|
baseRequest.files.add(assetRawUploadData);
|
||||||
|
|
||||||
onCurrentAsset(
|
onCurrentAsset(
|
||||||
CurrentUploadAsset(
|
CurrentUploadAsset(
|
||||||
id: entity.id,
|
id: asset.localId!,
|
||||||
fileCreatedAt: entity.createDateTime.year == 1970
|
fileCreatedAt: asset.fileCreatedAt.year == 1970
|
||||||
? entity.modifiedDateTime
|
? asset.fileModifiedAt
|
||||||
: entity.createDateTime,
|
: asset.fileCreatedAt,
|
||||||
fileName: originalFileName,
|
fileName: originalFileName,
|
||||||
fileType: _getAssetType(entity.type),
|
fileType: _getAssetType(asset.type),
|
||||||
fileSize: file.lengthSync(),
|
fileSize: file.lengthSync(),
|
||||||
iCloudAsset: false,
|
iCloudAsset: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
String? livePhotoVideoId;
|
String? livePhotoVideoId;
|
||||||
if (entity.isLivePhoto && livePhotoFile != null) {
|
if (asset.local!.isLivePhoto && livePhotoFile != null) {
|
||||||
livePhotoVideoId = await uploadLivePhotoVideo(
|
livePhotoVideoId = await uploadLivePhotoVideo(
|
||||||
originalFileName,
|
originalFileName,
|
||||||
livePhotoFile,
|
livePhotoFile,
|
||||||
@@ -448,16 +405,16 @@ class BackupService {
|
|||||||
final errorMessage = error['message'] ?? error['error'];
|
final errorMessage = error['message'] ?? error['error'];
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
|
"Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
|
||||||
);
|
);
|
||||||
|
|
||||||
onError(
|
onError(
|
||||||
ErrorUploadAsset(
|
ErrorUploadAsset(
|
||||||
asset: entity,
|
asset: asset,
|
||||||
id: entity.id,
|
id: asset.localId!,
|
||||||
fileCreatedAt: entity.createDateTime,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
fileName: originalFileName,
|
fileName: originalFileName,
|
||||||
fileType: _getAssetType(entity.type),
|
fileType: _getAssetType(candidate.asset.type),
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -473,7 +430,7 @@ class BackupService {
|
|||||||
bool isDuplicate = false;
|
bool isDuplicate = false;
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
isDuplicate = true;
|
isDuplicate = true;
|
||||||
duplicatedAssetIds.add(entity.id);
|
duplicatedAssetIds.add(asset.localId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess(
|
onSuccess(
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart' show PhotoManager;
|
|
||||||
|
|
||||||
/// Finds duplicates originating from missing EXIF information
|
/// Finds duplicates originating from missing EXIF information
|
||||||
class BackupVerificationService {
|
class BackupVerificationService {
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
|
|
||||||
BackupVerificationService(this._db);
|
BackupVerificationService(this._db, this._fileMediaRepository);
|
||||||
|
|
||||||
/// Returns at most [limit] assets that were backed up without exif
|
/// Returns at most [limit] assets that were backed up without exif
|
||||||
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
|
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
|
||||||
@@ -71,6 +73,7 @@ class BackupVerificationService {
|
|||||||
auth: Store.get(StoreKey.accessToken),
|
auth: Store.get(StoreKey.accessToken),
|
||||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||||
rootIsolateToken: isolateToken,
|
rootIsolateToken: isolateToken,
|
||||||
|
fileMediaRepository: _fileMediaRepository,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final upper = compute(
|
final upper = compute(
|
||||||
@@ -81,6 +84,7 @@ class BackupVerificationService {
|
|||||||
auth: Store.get(StoreKey.accessToken),
|
auth: Store.get(StoreKey.accessToken),
|
||||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||||
rootIsolateToken: isolateToken,
|
rootIsolateToken: isolateToken,
|
||||||
|
fileMediaRepository: _fileMediaRepository,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
toDelete = await lower + await upper;
|
toDelete = await lower + await upper;
|
||||||
@@ -93,6 +97,7 @@ class BackupVerificationService {
|
|||||||
auth: Store.get(StoreKey.accessToken),
|
auth: Store.get(StoreKey.accessToken),
|
||||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||||
rootIsolateToken: isolateToken,
|
rootIsolateToken: isolateToken,
|
||||||
|
fileMediaRepository: _fileMediaRepository,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -106,12 +111,13 @@ class BackupVerificationService {
|
|||||||
String auth,
|
String auth,
|
||||||
String endpoint,
|
String endpoint,
|
||||||
RootIsolateToken rootIsolateToken,
|
RootIsolateToken rootIsolateToken,
|
||||||
|
IFileMediaRepository fileMediaRepository,
|
||||||
}) tuple,
|
}) tuple,
|
||||||
) async {
|
) async {
|
||||||
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
||||||
final List<Asset> result = [];
|
final List<Asset> result = [];
|
||||||
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
||||||
await PhotoManager.setIgnorePermissionCheck(true);
|
await tuple.fileMediaRepository.enableBackgroundAccess();
|
||||||
final ApiService apiService = ApiService();
|
final ApiService apiService = ApiService();
|
||||||
apiService.setEndpoint(tuple.endpoint);
|
apiService.setEndpoint(tuple.endpoint);
|
||||||
apiService.setAccessToken(tuple.auth);
|
apiService.setAccessToken(tuple.auth);
|
||||||
@@ -228,5 +234,6 @@ class BackupVerificationService {
|
|||||||
final backupVerificationServiceProvider = Provider(
|
final backupVerificationServiceProvider = Provider(
|
||||||
(ref) => BackupVerificationService(
|
(ref) => BackupVerificationService(
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||||
|
|
||||||
|
class EntityService {
|
||||||
|
final IAssetRepository _assetRepository;
|
||||||
|
final IUserRepository _userRepository;
|
||||||
|
EntityService(
|
||||||
|
this._assetRepository,
|
||||||
|
this._userRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
|
||||||
|
final ownerId = album.ownerId;
|
||||||
|
if (ownerId != null) {
|
||||||
|
// replace owner with user from database
|
||||||
|
album.owner.value = await _userRepository.get(ownerId);
|
||||||
|
}
|
||||||
|
final thumbnailAssetId =
|
||||||
|
album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId;
|
||||||
|
if (thumbnailAssetId != null) {
|
||||||
|
// set thumbnail with asset from database
|
||||||
|
album.thumbnail.value =
|
||||||
|
await _assetRepository.getByRemoteId(thumbnailAssetId);
|
||||||
|
}
|
||||||
|
if (album.remoteUsers.isNotEmpty) {
|
||||||
|
// replace all users with users from database
|
||||||
|
final users = await _userRepository
|
||||||
|
.getByIds(album.remoteUsers.map((user) => user.id).toList());
|
||||||
|
album.sharedUsers.clear();
|
||||||
|
album.sharedUsers.addAll(users);
|
||||||
|
}
|
||||||
|
if (album.remoteAssets.isNotEmpty) {
|
||||||
|
// replace all assets with assets from database
|
||||||
|
final assets = await _assetRepository
|
||||||
|
.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!));
|
||||||
|
album.assets.clear();
|
||||||
|
album.assets.addAll(assets);
|
||||||
|
}
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final entityServiceProvider = Provider(
|
||||||
|
(ref) => EntityService(
|
||||||
|
ref.watch(assetRepositoryProvider),
|
||||||
|
ref.watch(userRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -2,6 +2,9 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
@@ -11,38 +14,46 @@ import 'package:immich_mobile/providers/db.provider.dart';
|
|||||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
class HashService {
|
class HashService {
|
||||||
HashService(this._db, this._backgroundService);
|
HashService(this._db, this._backgroundService, this._albumMediaRepository);
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final BackgroundService _backgroundService;
|
final BackgroundService _backgroundService;
|
||||||
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
final _log = Logger('HashService');
|
final _log = Logger('HashService');
|
||||||
|
|
||||||
/// Returns all assets that were successfully hashed
|
/// Returns all assets that were successfully hashed
|
||||||
Future<List<Asset>> getHashedAssets(
|
Future<List<Asset>> getHashedAssets(
|
||||||
AssetPathEntity album, {
|
Album album, {
|
||||||
int start = 0,
|
int start = 0,
|
||||||
int end = 0x7fffffffffffffff,
|
int end = 0x7fffffffffffffff,
|
||||||
|
DateTime? modifiedFrom,
|
||||||
|
DateTime? modifiedUntil,
|
||||||
Set<String>? excludedAssets,
|
Set<String>? excludedAssets,
|
||||||
}) async {
|
}) async {
|
||||||
final entities = await album.getAssetListRange(start: start, end: end);
|
final entities = await _albumMediaRepository.getAssets(
|
||||||
|
album.localId!,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
modifiedFrom: modifiedFrom,
|
||||||
|
modifiedUntil: modifiedUntil,
|
||||||
|
);
|
||||||
final filtered = excludedAssets == null
|
final filtered = excludedAssets == null
|
||||||
? entities
|
? entities
|
||||||
: entities.where((e) => !excludedAssets.contains(e.id)).toList();
|
: entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
|
||||||
return _hashAssets(filtered);
|
return _hashAssets(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a list of [AssetEntity]s to [Asset]s including only those
|
/// Processes a list of local [Asset]s, storing their hash and returning only those
|
||||||
/// that were successfully hashed. Hashes are looked up in a DB table
|
/// that were successfully hashed. Hashes are looked up in a DB table
|
||||||
/// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
|
/// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
|
||||||
/// entries are newly hashed and added to the DB table.
|
/// entries are newly hashed and added to the DB table.
|
||||||
Future<List<Asset>> _hashAssets(List<AssetEntity> assetEntities) async {
|
Future<List<Asset>> _hashAssets(List<Asset> assets) async {
|
||||||
const int batchFileCount = 128;
|
const int batchFileCount = 128;
|
||||||
const int batchDataSize = 1024 * 1024 * 1024; // 1GB
|
const int batchDataSize = 1024 * 1024 * 1024; // 1GB
|
||||||
|
|
||||||
final ids = assetEntities
|
final ids = assets
|
||||||
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
.map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
|
||||||
.toList();
|
.toList();
|
||||||
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
||||||
final List<DeviceAsset> toAdd = [];
|
final List<DeviceAsset> toAdd = [];
|
||||||
@@ -50,22 +61,28 @@ class HashService {
|
|||||||
|
|
||||||
int bytes = 0;
|
int bytes = 0;
|
||||||
|
|
||||||
for (int i = 0; i < assetEntities.length; i++) {
|
for (int i = 0; i < assets.length; i++) {
|
||||||
if (hashes[i] != null) {
|
if (hashes[i] != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final file = await assetEntities[i].originFile;
|
|
||||||
if (file == null) {
|
|
||||||
final fileName = await assetEntities[i].titleAsync.catchError((error) {
|
|
||||||
_log.warning(
|
|
||||||
"Failed to get title for asset ${assetEntities[i].id}",
|
|
||||||
);
|
|
||||||
|
|
||||||
return "";
|
File? file;
|
||||||
});
|
|
||||||
|
try {
|
||||||
|
file = await assets[i].local!.originFile;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_log.warning(
|
||||||
|
"Error getting file to hash for asset ${assets[i].localId}, name: ${assets[i].fileName}, created on: ${assets[i].fileCreatedAt}, skipping",
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
final fileName = assets[i].fileName;
|
||||||
|
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"Failed to get file for asset ${assetEntities[i].id}, name: $fileName, created on: ${assetEntities[i].createDateTime}, skipping",
|
"Failed to get file for asset ${assets[i].localId}, name: $fileName, created on: ${assets[i].fileCreatedAt}, skipping",
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -86,7 +103,7 @@ class HashService {
|
|||||||
if (toHash.isNotEmpty) {
|
if (toHash.isNotEmpty) {
|
||||||
await _processBatch(toHash, toAdd);
|
await _processBatch(toHash, toAdd);
|
||||||
}
|
}
|
||||||
return _mapAllHashedAssets(assetEntities, hashes);
|
return _getHashedAssets(assets, hashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup hashes of assets by their local ID
|
/// Lookup hashes of assets by their local ID
|
||||||
@@ -133,15 +150,16 @@ class HashService {
|
|||||||
return hashes;
|
return hashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts [AssetEntity]s that were successfully hashed to [Asset]s
|
/// Returns all successfully hashed [Asset]s with their hash value set
|
||||||
List<Asset> _mapAllHashedAssets(
|
List<Asset> _getHashedAssets(
|
||||||
List<AssetEntity> assets,
|
List<Asset> assets,
|
||||||
List<DeviceAsset?> hashes,
|
List<DeviceAsset?> hashes,
|
||||||
) {
|
) {
|
||||||
final List<Asset> result = [];
|
final List<Asset> result = [];
|
||||||
for (int i = 0; i < assets.length; i++) {
|
for (int i = 0; i < assets.length; i++) {
|
||||||
if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) {
|
if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) {
|
||||||
result.add(Asset.local(assets[i], hashes[i]!.hash));
|
assets[i].byteHash = hashes[i]!.hash;
|
||||||
|
result.add(assets[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -152,5 +170,6 @@ final hashServiceProvider = Provider(
|
|||||||
(ref) => HashService(
|
(ref) => HashService(
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
ref.watch(backgroundServiceProvider),
|
ref.watch(backgroundServiceProvider),
|
||||||
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,21 +3,27 @@ import 'dart:io';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
final imageViewerServiceProvider =
|
final imageViewerServiceProvider = Provider(
|
||||||
Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
|
(ref) => ImageViewerService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class ImageViewerService {
|
class ImageViewerService {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
final Logger _log = Logger("ImageViewerService");
|
final Logger _log = Logger("ImageViewerService");
|
||||||
|
|
||||||
ImageViewerService(this._apiService);
|
ImageViewerService(this._apiService, this._fileMediaRepository);
|
||||||
|
|
||||||
Future<bool> downloadAsset(Asset asset) async {
|
Future<bool> downloadAsset(Asset asset) async {
|
||||||
File? imageFile;
|
File? imageFile;
|
||||||
@@ -46,7 +52,7 @@ class ImageViewerService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetEntity? entity;
|
Asset? resultAsset;
|
||||||
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
videoFile = await File('${tempDir.path}/livephoto.mov').create();
|
videoFile = await File('${tempDir.path}/livephoto.mov').create();
|
||||||
@@ -54,24 +60,21 @@ class ImageViewerService {
|
|||||||
videoFile.writeAsBytesSync(motionResponse.bodyBytes);
|
videoFile.writeAsBytesSync(motionResponse.bodyBytes);
|
||||||
imageFile.writeAsBytesSync(imageResponse.bodyBytes);
|
imageFile.writeAsBytesSync(imageResponse.bodyBytes);
|
||||||
|
|
||||||
entity = await PhotoManager.editor.darwin.saveLivePhoto(
|
resultAsset = await _fileMediaRepository.saveLivePhoto(
|
||||||
imageFile: imageFile,
|
image: imageFile,
|
||||||
videoFile: videoFile,
|
video: videoFile,
|
||||||
title: asset.fileName,
|
title: asset.fileName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (entity == null) {
|
if (resultAsset == null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
|
"Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
|
||||||
);
|
);
|
||||||
|
resultAsset = await _fileMediaRepository
|
||||||
entity = await PhotoManager.editor.saveImage(
|
.saveImage(imageResponse.bodyBytes, title: asset.fileName);
|
||||||
imageResponse.bodyBytes,
|
|
||||||
title: asset.fileName,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity != null;
|
return resultAsset != null;
|
||||||
} else {
|
} else {
|
||||||
var res = await _apiService.assetsApi
|
var res = await _apiService.assetsApi
|
||||||
.downloadAssetWithHttpInfo(asset.remoteId!);
|
.downloadAssetWithHttpInfo(asset.remoteId!);
|
||||||
@@ -81,11 +84,11 @@ class ImageViewerService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final Asset? resultAsset;
|
||||||
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
|
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
|
||||||
|
|
||||||
if (asset.isImage) {
|
if (asset.isImage) {
|
||||||
entity = await PhotoManager.editor.saveImage(
|
resultAsset = await _fileMediaRepository.saveImage(
|
||||||
res.bodyBytes,
|
res.bodyBytes,
|
||||||
title: asset.fileName,
|
title: asset.fileName,
|
||||||
relativePath: relativePath,
|
relativePath: relativePath,
|
||||||
@@ -94,13 +97,13 @@ class ImageViewerService {
|
|||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
|
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
|
||||||
videoFile.writeAsBytesSync(res.bodyBytes);
|
videoFile.writeAsBytesSync(res.bodyBytes);
|
||||||
entity = await PhotoManager.editor.saveVideo(
|
resultAsset = await _fileMediaRepository.saveVideo(
|
||||||
videoFile,
|
videoFile,
|
||||||
title: asset.fileName,
|
title: asset.fileName,
|
||||||
relativePath: relativePath,
|
relativePath: relativePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return entity != null;
|
return resultAsset != null;
|
||||||
}
|
}
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_log.severe("Error saving downloaded asset", error, stack);
|
_log.severe("Error saving downloaded asset", error, stack);
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import 'package:immich_mobile/entities/etag.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
@@ -16,20 +21,33 @@ import 'package:immich_mobile/utils/datetime_comparison.dart';
|
|||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
final syncServiceProvider = Provider(
|
final syncServiceProvider = Provider(
|
||||||
(ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)),
|
(ref) => SyncService(
|
||||||
|
ref.watch(dbProvider),
|
||||||
|
ref.watch(hashServiceProvider),
|
||||||
|
ref.watch(entityServiceProvider),
|
||||||
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
|
ref.watch(albumApiRepositoryProvider),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class SyncService {
|
class SyncService {
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final HashService _hashService;
|
final HashService _hashService;
|
||||||
|
final EntityService _entityService;
|
||||||
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
|
final IAlbumApiRepository _albumApiRepository;
|
||||||
final AsyncMutex _lock = AsyncMutex();
|
final AsyncMutex _lock = AsyncMutex();
|
||||||
final Logger _log = Logger('SyncService');
|
final Logger _log = Logger('SyncService');
|
||||||
|
|
||||||
SyncService(this._db, this._hashService);
|
SyncService(
|
||||||
|
this._db,
|
||||||
|
this._hashService,
|
||||||
|
this._entityService,
|
||||||
|
this._albumMediaRepository,
|
||||||
|
this._albumApiRepository,
|
||||||
|
);
|
||||||
|
|
||||||
// public methods:
|
// public methods:
|
||||||
|
|
||||||
@@ -59,16 +77,15 @@ class SyncService {
|
|||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> syncRemoteAlbumsToDb(
|
Future<bool> syncRemoteAlbumsToDb(
|
||||||
List<AlbumResponseDto> remote, {
|
List<Album> remote, {
|
||||||
required bool isShared,
|
required bool isShared,
|
||||||
required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
}) =>
|
}) =>
|
||||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
|
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
|
||||||
|
|
||||||
/// Syncs all device albums and their assets to the database
|
/// Syncs all device albums and their assets to the database
|
||||||
/// Returns `true` if there were any changes
|
/// Returns `true` if there were any changes
|
||||||
Future<bool> syncLocalAlbumAssetsToDb(
|
Future<bool> syncLocalAlbumAssetsToDb(
|
||||||
List<AssetPathEntity> onDevice, [
|
List<Album> onDevice, [
|
||||||
Set<String>? excludedAssets,
|
Set<String>? excludedAssets,
|
||||||
]) =>
|
]) =>
|
||||||
_lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets));
|
_lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets));
|
||||||
@@ -283,11 +300,10 @@ class SyncService {
|
|||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> _syncRemoteAlbumsToDb(
|
Future<bool> _syncRemoteAlbumsToDb(
|
||||||
List<AlbumResponseDto> remote,
|
List<Album> remoteAlbums,
|
||||||
bool isShared,
|
bool isShared,
|
||||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
) async {
|
) async {
|
||||||
remote.sortBy((e) => e.id);
|
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||||
|
|
||||||
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
||||||
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
||||||
@@ -304,14 +320,14 @@ class SyncService {
|
|||||||
final List<Asset> existing = [];
|
final List<Asset> existing = [];
|
||||||
|
|
||||||
final bool changes = await diffSortedLists(
|
final bool changes = await diffSortedLists(
|
||||||
remote,
|
remoteAlbums,
|
||||||
dbAlbums,
|
dbAlbums,
|
||||||
compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!),
|
compare: (remoteAlbum, dbAlbum) =>
|
||||||
both: (AlbumResponseDto a, Album b) =>
|
remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!),
|
||||||
_syncRemoteAlbum(a, b, toDelete, existing, loadDetails),
|
both: (remoteAlbum, dbAlbum) =>
|
||||||
onlyFirst: (AlbumResponseDto a) =>
|
_syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing),
|
||||||
_addAlbumFromServer(a, existing, loadDetails),
|
onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing),
|
||||||
onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete),
|
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isShared && toDelete.isNotEmpty) {
|
if (isShared && toDelete.isNotEmpty) {
|
||||||
@@ -332,26 +348,22 @@ class SyncService {
|
|||||||
/// syncing changes from local back to server)
|
/// syncing changes from local back to server)
|
||||||
/// accumulates
|
/// accumulates
|
||||||
Future<bool> _syncRemoteAlbum(
|
Future<bool> _syncRemoteAlbum(
|
||||||
AlbumResponseDto dto,
|
Album dto,
|
||||||
Album album,
|
Album album,
|
||||||
List<Asset> deleteCandidates,
|
List<Asset> deleteCandidates,
|
||||||
List<Asset> existing,
|
List<Asset> existing,
|
||||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
) async {
|
) async {
|
||||||
if (!_hasAlbumResponseDtoChanged(dto, album)) {
|
if (!_hasRemoteAlbumChanged(dto, album)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
|
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
|
||||||
// i.e. it will always be null. Save it here.
|
// i.e. it will always be null. Save it here.
|
||||||
final originalDto = dto;
|
final originalDto = dto;
|
||||||
dto = await loadDetails(dto);
|
dto = await _albumApiRepository.get(dto.remoteId!);
|
||||||
if (dto.assetCount != dto.assets.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final assetsInDb =
|
final assetsInDb =
|
||||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||||
final List<Asset> assetsOnRemote = dto.getAssets();
|
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
|
||||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||||
assetsOnRemote,
|
assetsOnRemote,
|
||||||
@@ -362,15 +374,16 @@ class SyncService {
|
|||||||
// update shared users
|
// update shared users
|
||||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||||
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
||||||
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
|
final List<User> users = dto.remoteUsers.toList()
|
||||||
|
..sort((a, b) => a.id.compareTo(b.id));
|
||||||
final List<String> userIdsToAdd = [];
|
final List<String> userIdsToAdd = [];
|
||||||
final List<User> usersToUnlink = [];
|
final List<User> usersToUnlink = [];
|
||||||
diffSortedListsSync(
|
diffSortedListsSync(
|
||||||
dto.albumUsers,
|
users,
|
||||||
sharedUsers,
|
sharedUsers,
|
||||||
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
|
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||||
both: (a, b) => false,
|
both: (a, b) => false,
|
||||||
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
|
onlyFirst: (User a) => userIdsToAdd.add(a.id),
|
||||||
onlySecond: (User a) => usersToUnlink.add(a),
|
onlySecond: (User a) => usersToUnlink.add(a),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -380,19 +393,19 @@ class SyncService {
|
|||||||
final assetsToLink = existingInDb + updated;
|
final assetsToLink = existingInDb + updated;
|
||||||
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
||||||
|
|
||||||
album.name = dto.albumName;
|
album.name = dto.name;
|
||||||
album.shared = dto.shared;
|
album.shared = dto.shared;
|
||||||
album.createdAt = dto.createdAt;
|
album.createdAt = dto.createdAt;
|
||||||
album.modifiedAt = dto.updatedAt;
|
album.modifiedAt = dto.modifiedAt;
|
||||||
album.startDate = dto.startDate;
|
album.startDate = dto.startDate;
|
||||||
album.endDate = dto.endDate;
|
album.endDate = dto.endDate;
|
||||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||||
album.shared = dto.shared;
|
album.shared = dto.shared;
|
||||||
album.activityEnabled = dto.isActivityEnabled;
|
album.activityEnabled = dto.activityEnabled;
|
||||||
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
|
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
|
||||||
album.thumbnail.value = await _db.assets
|
album.thumbnail.value = await _db.assets
|
||||||
.where()
|
.where()
|
||||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,27 +441,26 @@ class SyncService {
|
|||||||
/// (shared) assets to the database beforehand
|
/// (shared) assets to the database beforehand
|
||||||
/// accumulates assets already existing in the database
|
/// accumulates assets already existing in the database
|
||||||
Future<void> _addAlbumFromServer(
|
Future<void> _addAlbumFromServer(
|
||||||
AlbumResponseDto dto,
|
Album album,
|
||||||
List<Asset> existing,
|
List<Asset> existing,
|
||||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
) async {
|
) async {
|
||||||
if (dto.assetCount != dto.assets.length) {
|
if (album.remoteAssetCount != album.remoteAssets.length) {
|
||||||
dto = await loadDetails(dto);
|
album = await _albumApiRepository.get(album.remoteId!);
|
||||||
}
|
}
|
||||||
if (dto.assetCount == dto.assets.length) {
|
if (album.remoteAssetCount == album.remoteAssets.length) {
|
||||||
// in case an album contains assets not yet present in local DB:
|
// in case an album contains assets not yet present in local DB:
|
||||||
// put missing album assets into local DB
|
// put missing album assets into local DB
|
||||||
final (existingInDb, updated) =
|
final (existingInDb, updated) =
|
||||||
await _linkWithExistingFromDb(dto.getAssets());
|
await _linkWithExistingFromDb(album.remoteAssets.toList());
|
||||||
existing.addAll(existingInDb);
|
existing.addAll(existingInDb);
|
||||||
await upsertAssetsWithExif(updated);
|
await upsertAssetsWithExif(updated);
|
||||||
|
|
||||||
final Album a = await Album.remote(dto);
|
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||||
await _db.writeTxn(() => _db.albums.store(a));
|
await _db.writeTxn(() => _db.albums.store(album));
|
||||||
} else {
|
} else {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"Failed to add album from server: assetCount ${dto.assetCount} != "
|
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||||
"asset array length ${dto.assets.length} for album ${dto.albumName}");
|
"asset array length ${album.remoteAssets.length} for album ${album.name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +504,7 @@ class SyncService {
|
|||||||
/// Syncs all device albums and their assets to the database
|
/// Syncs all device albums and their assets to the database
|
||||||
/// Returns `true` if there were any changes
|
/// Returns `true` if there were any changes
|
||||||
Future<bool> _syncLocalAlbumAssetsToDb(
|
Future<bool> _syncLocalAlbumAssetsToDb(
|
||||||
List<AssetPathEntity> onDevice, [
|
List<Album> onDevice, [
|
||||||
Set<String>? excludedAssets,
|
Set<String>? excludedAssets,
|
||||||
]) async {
|
]) async {
|
||||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||||
@@ -504,16 +516,15 @@ class SyncService {
|
|||||||
final bool anyChanges = await diffSortedLists(
|
final bool anyChanges = await diffSortedLists(
|
||||||
onDevice,
|
onDevice,
|
||||||
inDb,
|
inDb,
|
||||||
compare: (AssetPathEntity a, Album b) => a.id.compareTo(b.localId!),
|
compare: (Album a, Album b) => a.localId!.compareTo(b.localId!),
|
||||||
both: (AssetPathEntity ape, Album album) => _syncAlbumInDbAndOnDevice(
|
both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(
|
||||||
ape,
|
a,
|
||||||
album,
|
b,
|
||||||
deleteCandidates,
|
deleteCandidates,
|
||||||
existing,
|
existing,
|
||||||
excludedAssets,
|
excludedAssets,
|
||||||
),
|
),
|
||||||
onlyFirst: (AssetPathEntity ape) =>
|
onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets),
|
||||||
_addAlbumFromDevice(ape, existing, excludedAssets),
|
|
||||||
onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
|
onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
|
||||||
);
|
);
|
||||||
_log.fine(
|
_log.fine(
|
||||||
@@ -541,58 +552,65 @@ class SyncService {
|
|||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
/// Accumulates asset candidates to delete and those already existing in DB
|
/// Accumulates asset candidates to delete and those already existing in DB
|
||||||
Future<bool> _syncAlbumInDbAndOnDevice(
|
Future<bool> _syncAlbumInDbAndOnDevice(
|
||||||
AssetPathEntity ape,
|
Album deviceAlbum,
|
||||||
Album album,
|
Album dbAlbum,
|
||||||
List<Asset> deleteCandidates,
|
List<Asset> deleteCandidates,
|
||||||
List<Asset> existing, [
|
List<Asset> existing, [
|
||||||
Set<String>? excludedAssets,
|
Set<String>? excludedAssets,
|
||||||
bool forceRefresh = false,
|
bool forceRefresh = false,
|
||||||
]) async {
|
]) async {
|
||||||
if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) {
|
if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) {
|
||||||
_log.fine("Local album ${ape.name} has not changed. Skipping sync.");
|
_log.fine(
|
||||||
|
"Local album ${deviceAlbum.name} has not changed. Skipping sync.",
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!forceRefresh &&
|
if (!forceRefresh &&
|
||||||
excludedAssets == null &&
|
excludedAssets == null &&
|
||||||
await _syncDeviceAlbumFast(ape, album)) {
|
await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
||||||
final inDb = await album.assets
|
final inDb = await dbAlbum.assets
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||||
.sortByChecksum()
|
.sortByChecksum()
|
||||||
.findAll();
|
.findAll();
|
||||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||||
final int assetCountOnDevice = await ape.assetCountAsync;
|
final int assetCountOnDevice =
|
||||||
final List<Asset> onDevice =
|
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
final List<Asset> onDevice = await _hashService.getHashedAssets(
|
||||||
|
deviceAlbum,
|
||||||
|
excludedAssets: excludedAssets,
|
||||||
|
);
|
||||||
_removeDuplicates(onDevice);
|
_removeDuplicates(onDevice);
|
||||||
// _removeDuplicates sorts `onDevice` by checksum
|
// _removeDuplicates sorts `onDevice` by checksum
|
||||||
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
||||||
if (toAdd.isEmpty &&
|
if (toAdd.isEmpty &&
|
||||||
toUpdate.isEmpty &&
|
toUpdate.isEmpty &&
|
||||||
toDelete.isEmpty &&
|
toDelete.isEmpty &&
|
||||||
album.name == ape.name &&
|
dbAlbum.name == deviceAlbum.name &&
|
||||||
ape.lastModified != null &&
|
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
|
||||||
album.modifiedAt.isAtSameMomentAs(ape.lastModified!)) {
|
|
||||||
// changes only affeted excluded albums
|
// changes only affeted excluded albums
|
||||||
_log.fine(
|
_log.fine(
|
||||||
"Only excluded assets in local album ${ape.name} changed. Stopping sync.",
|
"Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
|
||||||
);
|
);
|
||||||
if (assetCountOnDevice !=
|
if (assetCountOnDevice !=
|
||||||
_db.eTags.getByIdSync(ape.eTagKeyAssetCount)?.assetCount) {
|
_db.eTags.getByIdSync(deviceAlbum.eTagKeyAssetCount)?.assetCount) {
|
||||||
await _db.writeTxn(
|
await _db.writeTxn(
|
||||||
() => _db.eTags.put(
|
() => _db.eTags.put(
|
||||||
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
|
ETag(
|
||||||
|
id: deviceAlbum.eTagKeyAssetCount,
|
||||||
|
assetCount: assetCountOnDevice,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_log.fine(
|
_log.fine(
|
||||||
"Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
|
"Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
|
||||||
);
|
);
|
||||||
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
|
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
|
||||||
_log.fine(
|
_log.fine(
|
||||||
@@ -600,28 +618,31 @@ class SyncService {
|
|||||||
);
|
);
|
||||||
deleteCandidates.addAll(toDelete);
|
deleteCandidates.addAll(toDelete);
|
||||||
existing.addAll(existingInDb);
|
existing.addAll(existingInDb);
|
||||||
album.name = ape.name;
|
dbAlbum.name = deviceAlbum.name;
|
||||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
||||||
if (album.thumbnail.value != null &&
|
if (dbAlbum.thumbnail.value != null &&
|
||||||
toDelete.contains(album.thumbnail.value)) {
|
toDelete.contains(dbAlbum.thumbnail.value)) {
|
||||||
album.thumbnail.value = null;
|
dbAlbum.thumbnail.value = null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await _db.writeTxn(() async {
|
await _db.writeTxn(() async {
|
||||||
await _db.assets.putAll(updated);
|
await _db.assets.putAll(updated);
|
||||||
await _db.assets.putAll(toUpdate);
|
await _db.assets.putAll(toUpdate);
|
||||||
await album.assets
|
await dbAlbum.assets
|
||||||
.update(link: existingInDb + updated, unlink: toDelete);
|
.update(link: existingInDb + updated, unlink: toDelete);
|
||||||
await _db.albums.put(album);
|
await _db.albums.put(dbAlbum);
|
||||||
album.thumbnail.value ??= await album.assets.filter().findFirst();
|
dbAlbum.thumbnail.value ??= await dbAlbum.assets.filter().findFirst();
|
||||||
await album.thumbnail.save();
|
await dbAlbum.thumbnail.save();
|
||||||
await _db.eTags.put(
|
await _db.eTags.put(
|
||||||
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
|
ETag(
|
||||||
|
id: deviceAlbum.eTagKeyAssetCount,
|
||||||
|
assetCount: assetCountOnDevice,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
_log.info("Synced changes of local album ${ape.name} to DB");
|
_log.info("Synced changes of local album ${deviceAlbum.name} to DB");
|
||||||
} on IsarError catch (e) {
|
} on IsarError catch (e) {
|
||||||
_log.severe("Failed to update synced album ${ape.name} in DB", e);
|
_log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -629,45 +650,45 @@ class SyncService {
|
|||||||
|
|
||||||
/// fast path for common case: only new assets were added to device album
|
/// fast path for common case: only new assets were added to device album
|
||||||
/// returns `true` if successfull, else `false`
|
/// returns `true` if successfull, else `false`
|
||||||
Future<bool> _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async {
|
Future<bool> _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async {
|
||||||
if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
|
if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final int totalOnDevice = await ape.assetCountAsync;
|
final int totalOnDevice =
|
||||||
|
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||||
final int lastKnownTotal =
|
final int lastKnownTotal =
|
||||||
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0;
|
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ??
|
||||||
final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
|
0;
|
||||||
? await ape.fetchPathProperties(
|
if (totalOnDevice <= lastKnownTotal) {
|
||||||
filterOptionGroup: FilterOptionGroup(
|
|
||||||
updateTimeCond: DateTimeCond(
|
|
||||||
min: album.modifiedAt.add(const Duration(seconds: 1)),
|
|
||||||
max: ape.lastModified ?? DateTime.now(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
if (modified == null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
|
final List<Asset> newAssets = await _hashService.getHashedAssets(
|
||||||
|
deviceAlbum,
|
||||||
|
modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)),
|
||||||
|
modifiedUntil: deviceAlbum.modifiedAt,
|
||||||
|
);
|
||||||
|
|
||||||
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
||||||
_removeDuplicates(newAssets);
|
_removeDuplicates(newAssets);
|
||||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||||
try {
|
try {
|
||||||
await _db.writeTxn(() async {
|
await _db.writeTxn(() async {
|
||||||
await _db.assets.putAll(updated);
|
await _db.assets.putAll(updated);
|
||||||
await album.assets.update(link: existingInDb + updated);
|
await dbAlbum.assets.update(link: existingInDb + updated);
|
||||||
await _db.albums.put(album);
|
await _db.albums.put(dbAlbum);
|
||||||
await _db.eTags
|
await _db.eTags.put(
|
||||||
.put(ETag(id: ape.eTagKeyAssetCount, assetCount: totalOnDevice));
|
ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
_log.info("Fast synced local album ${ape.name} to DB");
|
_log.info("Fast synced local album ${deviceAlbum.name} to DB");
|
||||||
} on IsarError catch (e) {
|
} on IsarError catch (e) {
|
||||||
_log.severe("Failed to fast sync local album ${ape.name} to DB", e);
|
_log.severe(
|
||||||
|
"Failed to fast sync local album ${deviceAlbum.name} to DB",
|
||||||
|
e,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,14 +698,15 @@ class SyncService {
|
|||||||
/// Adds a new album from the device to the database and Accumulates all
|
/// Adds a new album from the device to the database and Accumulates all
|
||||||
/// assets already existing in the database to the list of `existing` assets
|
/// assets already existing in the database to the list of `existing` assets
|
||||||
Future<void> _addAlbumFromDevice(
|
Future<void> _addAlbumFromDevice(
|
||||||
AssetPathEntity ape,
|
Album album,
|
||||||
List<Asset> existing, [
|
List<Asset> existing, [
|
||||||
Set<String>? excludedAssets,
|
Set<String>? excludedAssets,
|
||||||
]) async {
|
]) async {
|
||||||
_log.info("Syncing a new local album to DB: ${ape.name}");
|
_log.info("Syncing a new local album to DB: ${album.name}");
|
||||||
final Album a = Album.local(ape);
|
final assets = await _hashService.getHashedAssets(
|
||||||
final assets =
|
album,
|
||||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
excludedAssets: excludedAssets,
|
||||||
|
);
|
||||||
_removeDuplicates(assets);
|
_removeDuplicates(assets);
|
||||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||||
_log.info(
|
_log.info(
|
||||||
@@ -692,15 +714,15 @@ class SyncService {
|
|||||||
);
|
);
|
||||||
await upsertAssetsWithExif(updated);
|
await upsertAssetsWithExif(updated);
|
||||||
existing.addAll(existingInDb);
|
existing.addAll(existingInDb);
|
||||||
a.assets.addAll(existingInDb);
|
album.assets.addAll(existingInDb);
|
||||||
a.assets.addAll(updated);
|
album.assets.addAll(updated);
|
||||||
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
|
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
|
||||||
a.thumbnail.value = thumb;
|
album.thumbnail.value = thumb;
|
||||||
try {
|
try {
|
||||||
await _db.writeTxn(() => _db.albums.store(a));
|
await _db.writeTxn(() => _db.albums.store(album));
|
||||||
_log.info("Added a new local album to DB: ${ape.name}");
|
_log.info("Added a new local album to DB: ${album.name}");
|
||||||
} on IsarError catch (e) {
|
} on IsarError catch (e) {
|
||||||
_log.severe("Failed to add new local album ${ape.name} to DB", e);
|
_log.severe("Failed to add new local album ${album.name} to DB", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,12 +820,15 @@ class SyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// returns `true` if the albums differ on the surface
|
/// returns `true` if the albums differ on the surface
|
||||||
Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
|
Future<bool> _hasAlbumChangeOnDevice(
|
||||||
return a.name != b.name ||
|
Album deviceAlbum,
|
||||||
a.lastModified == null ||
|
Album dbAlbum,
|
||||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
|
) async {
|
||||||
await a.assetCountAsync !=
|
return deviceAlbum.name != dbAlbum.name ||
|
||||||
(await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
|
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||||
|
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
|
||||||
|
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))
|
||||||
|
?.assetCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _removeAllLocalAlbumsAndAssets() async {
|
Future<bool> _removeAllLocalAlbumsAndAssets() async {
|
||||||
@@ -900,17 +925,17 @@ class SyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// returns `true` if the albums differ on the surface
|
/// returns `true` if the albums differ on the surface
|
||||||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
|
||||||
return dto.assetCount != a.assetCount ||
|
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
|
||||||
dto.albumName != a.name ||
|
remoteAlbum.name != dbAlbum.name ||
|
||||||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
|
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
|
||||||
dto.shared != a.shared ||
|
remoteAlbum.shared != dbAlbum.shared ||
|
||||||
dto.albumUsers.length != a.sharedUsers.length ||
|
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
|
||||||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
!remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||||
!isAtSameMomentAs(dto.startDate, a.startDate) ||
|
!isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) ||
|
||||||
!isAtSameMomentAs(dto.endDate, a.endDate) ||
|
!isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) ||
|
||||||
!isAtSameMomentAs(
|
!isAtSameMomentAs(
|
||||||
dto.lastModifiedAssetTimestamp,
|
remoteAlbum.lastModifiedAssetTimestamp,
|
||||||
a.lastModifiedAssetTimestamp,
|
dbAlbum.lastModifiedAssetTimestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,29 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
addDefault(value, 'tags', TagsResponse().toJson());
|
addDefault(value, 'tags', TagsResponse().toJson());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'ServerConfigDto':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(
|
||||||
|
value,
|
||||||
|
'mapLightStyleUrl',
|
||||||
|
'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
);
|
||||||
|
addDefault(
|
||||||
|
value,
|
||||||
|
'mapDarkStyleUrl',
|
||||||
|
'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'UserResponseDto':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'UserAdminResponseDto':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,23 +183,13 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
padding: const EdgeInsets.only(top: 2.0),
|
||||||
child: FutureBuilder(
|
child: Text(
|
||||||
builder: ((context, snapshot) {
|
album.assetCount.toString() +
|
||||||
if (snapshot.hasData) {
|
(album.isAll ? " (${'backup_all'.tr()})" : ""),
|
||||||
return Text(
|
style: TextStyle(
|
||||||
snapshot.data.toString() +
|
fontSize: 12,
|
||||||
(album.isAll
|
color: Colors.grey[600],
|
||||||
? " (${'backup_all'.tr()})"
|
),
|
||||||
: ""),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const Text("0");
|
|
||||||
}),
|
|
||||||
future: album.assetCount,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -208,7 +198,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushRoute(
|
context.pushRoute(
|
||||||
AlbumPreviewRoute(album: album.albumEntity),
|
AlbumPreviewRoute(album: album.album),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
@@ -24,19 +23,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
|||||||
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
|
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
|
||||||
final bool isExcluded =
|
final bool isExcluded =
|
||||||
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
|
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
|
||||||
final assetCount = useState(0);
|
|
||||||
final syncAlbum = ref
|
final syncAlbum = ref
|
||||||
.watch(appSettingsServiceProvider)
|
.watch(appSettingsServiceProvider)
|
||||||
.getSetting(AppSettingsEnum.syncAlbums);
|
.getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
album.assetCount.then((value) => assetCount.value = value);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[album],
|
|
||||||
);
|
|
||||||
|
|
||||||
buildTileColor() {
|
buildTileColor() {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return context.isDarkTheme
|
return context.isDarkTheme
|
||||||
@@ -117,11 +107,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(assetCount.value.toString()),
|
subtitle: Text(album.assetCount.toString()),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushRoute(
|
context.pushRoute(
|
||||||
AlbumPreviewRoute(album: album.albumEntity),
|
AlbumPreviewRoute(album: album.album),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
import 'package:immich_mobile/providers/backup/error_backup_list.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/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
|
|
||||||
class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||||
const CurrentUploadingAssetInfoBox({super.key});
|
const CurrentUploadingAssetInfoBox({super.key});
|
||||||
@@ -148,17 +149,6 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildAssetThumbnail() async {
|
|
||||||
var assetEntity = await AssetEntity.fromId(asset.id);
|
|
||||||
|
|
||||||
if (assetEntity != null) {
|
|
||||||
return assetEntity.thumbnailDataWithSize(
|
|
||||||
const ThumbnailSize(500, 500),
|
|
||||||
quality: 100,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildiCloudDownloadProgerssBar() {
|
buildiCloudDownloadProgerssBar() {
|
||||||
if (asset.iCloudAsset != null && asset.iCloudAsset!) {
|
if (asset.iCloudAsset != null && asset.iCloudAsset!) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -239,8 +229,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FutureBuilder<Uint8List?>(
|
return FutureBuilder<Asset?>(
|
||||||
future: buildAssetThumbnail(),
|
future: ref.read(assetMediaRepositoryProvider).get(asset.id),
|
||||||
builder: (context, thumbnail) => ListTile(
|
builder: (context, thumbnail) => ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: AnimatedCrossFade(
|
leading: AnimatedCrossFade(
|
||||||
@@ -250,9 +240,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
child: thumbnail.hasData
|
child: thumbnail.hasData
|
||||||
? ClipRRect(
|
? ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
child: Image.memory(
|
child: ImmichThumbnail(
|
||||||
thumbnail.data!,
|
asset: thumbnail.data,
|
||||||
fit: BoxFit.cover,
|
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
),
|
),
|
||||||
|
|||||||
Generated
+7
-2
@@ -116,6 +116,7 @@ Class | Method | HTTP request | Description
|
|||||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||||
*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets |
|
*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets |
|
||||||
|
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
||||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
||||||
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
|
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
|
||||||
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
|
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
|
||||||
@@ -124,6 +125,7 @@ Class | Method | HTTP request | Description
|
|||||||
*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix |
|
*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix |
|
||||||
*FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports |
|
*FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports |
|
||||||
*FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum |
|
*FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum |
|
||||||
|
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs |
|
||||||
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
|
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
|
||||||
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||||
*LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries |
|
*LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries |
|
||||||
@@ -136,7 +138,6 @@ Class | Method | HTTP request | Description
|
|||||||
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} |
|
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} |
|
||||||
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |
|
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |
|
||||||
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |
|
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |
|
||||||
*MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json |
|
|
||||||
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode |
|
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode |
|
||||||
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
|
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
|
||||||
*MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |
|
*MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |
|
||||||
@@ -171,6 +172,7 @@ Class | Method | HTTP request | Description
|
|||||||
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata |
|
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata |
|
||||||
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
||||||
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
||||||
|
*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random |
|
||||||
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
|
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
|
||||||
*ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license |
|
*ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license |
|
||||||
*ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about |
|
*ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about |
|
||||||
@@ -330,6 +332,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [JobCommand](doc//JobCommand.md)
|
- [JobCommand](doc//JobCommand.md)
|
||||||
- [JobCommandDto](doc//JobCommandDto.md)
|
- [JobCommandDto](doc//JobCommandDto.md)
|
||||||
- [JobCountsDto](doc//JobCountsDto.md)
|
- [JobCountsDto](doc//JobCountsDto.md)
|
||||||
|
- [JobCreateDto](doc//JobCreateDto.md)
|
||||||
- [JobName](doc//JobName.md)
|
- [JobName](doc//JobName.md)
|
||||||
- [JobSettingsDto](doc//JobSettingsDto.md)
|
- [JobSettingsDto](doc//JobSettingsDto.md)
|
||||||
- [JobStatusDto](doc//JobStatusDto.md)
|
- [JobStatusDto](doc//JobStatusDto.md)
|
||||||
@@ -341,9 +344,9 @@ Class | Method | HTTP request | Description
|
|||||||
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
||||||
- [LoginResponseDto](doc//LoginResponseDto.md)
|
- [LoginResponseDto](doc//LoginResponseDto.md)
|
||||||
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
||||||
|
- [ManualJobName](doc//ManualJobName.md)
|
||||||
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
||||||
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
|
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
|
||||||
- [MapTheme](doc//MapTheme.md)
|
|
||||||
- [MemoriesResponse](doc//MemoriesResponse.md)
|
- [MemoriesResponse](doc//MemoriesResponse.md)
|
||||||
- [MemoriesUpdate](doc//MemoriesUpdate.md)
|
- [MemoriesUpdate](doc//MemoriesUpdate.md)
|
||||||
- [MemoryCreateDto](doc//MemoryCreateDto.md)
|
- [MemoryCreateDto](doc//MemoryCreateDto.md)
|
||||||
@@ -376,6 +379,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||||
|
- [RandomSearchDto](doc//RandomSearchDto.md)
|
||||||
- [RatingsResponse](doc//RatingsResponse.md)
|
- [RatingsResponse](doc//RatingsResponse.md)
|
||||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||||
- [ReactionLevel](doc//ReactionLevel.md)
|
- [ReactionLevel](doc//ReactionLevel.md)
|
||||||
@@ -449,6 +453,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [ToneMapping](doc//ToneMapping.md)
|
- [ToneMapping](doc//ToneMapping.md)
|
||||||
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
|
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
|
||||||
- [TranscodePolicy](doc//TranscodePolicy.md)
|
- [TranscodePolicy](doc//TranscodePolicy.md)
|
||||||
|
- [TrashResponseDto](doc//TrashResponseDto.md)
|
||||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||||
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
||||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||||
|
|||||||
Generated
+4
-1
@@ -144,6 +144,7 @@ part 'model/image_format.dart';
|
|||||||
part 'model/job_command.dart';
|
part 'model/job_command.dart';
|
||||||
part 'model/job_command_dto.dart';
|
part 'model/job_command_dto.dart';
|
||||||
part 'model/job_counts_dto.dart';
|
part 'model/job_counts_dto.dart';
|
||||||
|
part 'model/job_create_dto.dart';
|
||||||
part 'model/job_name.dart';
|
part 'model/job_name.dart';
|
||||||
part 'model/job_settings_dto.dart';
|
part 'model/job_settings_dto.dart';
|
||||||
part 'model/job_status_dto.dart';
|
part 'model/job_status_dto.dart';
|
||||||
@@ -155,9 +156,9 @@ part 'model/log_level.dart';
|
|||||||
part 'model/login_credential_dto.dart';
|
part 'model/login_credential_dto.dart';
|
||||||
part 'model/login_response_dto.dart';
|
part 'model/login_response_dto.dart';
|
||||||
part 'model/logout_response_dto.dart';
|
part 'model/logout_response_dto.dart';
|
||||||
|
part 'model/manual_job_name.dart';
|
||||||
part 'model/map_marker_response_dto.dart';
|
part 'model/map_marker_response_dto.dart';
|
||||||
part 'model/map_reverse_geocode_response_dto.dart';
|
part 'model/map_reverse_geocode_response_dto.dart';
|
||||||
part 'model/map_theme.dart';
|
|
||||||
part 'model/memories_response.dart';
|
part 'model/memories_response.dart';
|
||||||
part 'model/memories_update.dart';
|
part 'model/memories_update.dart';
|
||||||
part 'model/memory_create_dto.dart';
|
part 'model/memory_create_dto.dart';
|
||||||
@@ -190,6 +191,7 @@ part 'model/places_response_dto.dart';
|
|||||||
part 'model/purchase_response.dart';
|
part 'model/purchase_response.dart';
|
||||||
part 'model/purchase_update.dart';
|
part 'model/purchase_update.dart';
|
||||||
part 'model/queue_status_dto.dart';
|
part 'model/queue_status_dto.dart';
|
||||||
|
part 'model/random_search_dto.dart';
|
||||||
part 'model/ratings_response.dart';
|
part 'model/ratings_response.dart';
|
||||||
part 'model/ratings_update.dart';
|
part 'model/ratings_update.dart';
|
||||||
part 'model/reaction_level.dart';
|
part 'model/reaction_level.dart';
|
||||||
@@ -263,6 +265,7 @@ part 'model/time_bucket_size.dart';
|
|||||||
part 'model/tone_mapping.dart';
|
part 'model/tone_mapping.dart';
|
||||||
part 'model/transcode_hw_accel.dart';
|
part 'model/transcode_hw_accel.dart';
|
||||||
part 'model/transcode_policy.dart';
|
part 'model/transcode_policy.dart';
|
||||||
|
part 'model/trash_response_dto.dart';
|
||||||
part 'model/update_album_dto.dart';
|
part 'model/update_album_dto.dart';
|
||||||
part 'model/update_album_user_dto.dart';
|
part 'model/update_album_user_dto.dart';
|
||||||
part 'model/update_asset_dto.dart';
|
part 'model/update_asset_dto.dart';
|
||||||
|
|||||||
Generated
+6
-1
@@ -449,7 +449,10 @@ class AssetsApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /assets/random' operation and returns the [Response].
|
/// This property was deprecated in v1.116.0
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [num] count:
|
/// * [num] count:
|
||||||
@@ -482,6 +485,8 @@ class AssetsApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This property was deprecated in v1.116.0
|
||||||
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [num] count:
|
/// * [num] count:
|
||||||
|
|||||||
+59
@@ -71,4 +71,63 @@ class DeprecatedApi {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This property was deprecated in v1.116.0
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [num] count:
|
||||||
|
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/assets/random';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (count != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'count', count));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This property was deprecated in v1.116.0
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [num] count:
|
||||||
|
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||||
|
final response = await getRandomWithHttpInfo( count: count, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
||||||
|
.cast<AssetResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+39
@@ -16,6 +16,45 @@ class JobsApi {
|
|||||||
|
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /jobs' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [JobCreateDto] jobCreateDto (required):
|
||||||
|
Future<Response> createJobWithHttpInfo(JobCreateDto jobCreateDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/jobs';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = jobCreateDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [JobCreateDto] jobCreateDto (required):
|
||||||
|
Future<void> createJob(JobCreateDto jobCreateDto,) async {
|
||||||
|
final response = await createJobWithHttpInfo(jobCreateDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /jobs' operation and returns the [Response].
|
/// Performs an HTTP 'GET /jobs' operation and returns the [Response].
|
||||||
Future<Response> getAllJobsStatusWithHttpInfo() async {
|
Future<Response> getAllJobsStatusWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
|
|||||||
Generated
-56
@@ -105,62 +105,6 @@ class MapApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /map/style.json' operation and returns the [Response].
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [MapTheme] theme (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
Future<Response> getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final path = r'/map/style.json';
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody;
|
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
|
||||||
final headerParams = <String, String>{};
|
|
||||||
final formParams = <String, String>{};
|
|
||||||
|
|
||||||
if (key != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
|
||||||
}
|
|
||||||
queryParams.addAll(_queryParams('', 'theme', theme));
|
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
|
||||||
|
|
||||||
|
|
||||||
return apiClient.invokeAPI(
|
|
||||||
path,
|
|
||||||
'GET',
|
|
||||||
queryParams,
|
|
||||||
postBody,
|
|
||||||
headerParams,
|
|
||||||
formParams,
|
|
||||||
contentTypes.isEmpty ? null : contentTypes.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [MapTheme] theme (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
Future<Object?> getMapStyle(MapTheme theme, { String? key, }) async {
|
|
||||||
final response = await getMapStyleWithHttpInfo(theme, key: key, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
|
||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
|
||||||
// FormatException when trying to decode an empty string.
|
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
|
||||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object;
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response].
|
/// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
Generated
+47
@@ -351,6 +351,53 @@ class SearchApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /search/random' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [RandomSearchDto] randomSearchDto (required):
|
||||||
|
Future<Response> searchRandomWithHttpInfo(RandomSearchDto randomSearchDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/search/random';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = randomSearchDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [RandomSearchDto] randomSearchDto (required):
|
||||||
|
Future<SearchResponseDto?> searchRandom(RandomSearchDto randomSearchDto,) async {
|
||||||
|
final response = await searchRandomWithHttpInfo(randomSearchDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /search/smart' operation and returns the [Response].
|
/// Performs an HTTP 'POST /search/smart' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
Generated
+27
-3
@@ -42,11 +42,19 @@ class TrashApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> emptyTrash() async {
|
Future<TrashResponseDto?> emptyTrash() async {
|
||||||
final response = await emptyTrashWithHttpInfo();
|
final response = await emptyTrashWithHttpInfo();
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /trash/restore/assets' operation and returns the [Response].
|
/// Performs an HTTP 'POST /trash/restore/assets' operation and returns the [Response].
|
||||||
@@ -81,11 +89,19 @@ class TrashApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [BulkIdsDto] bulkIdsDto (required):
|
/// * [BulkIdsDto] bulkIdsDto (required):
|
||||||
Future<void> restoreAssets(BulkIdsDto bulkIdsDto,) async {
|
Future<TrashResponseDto?> restoreAssets(BulkIdsDto bulkIdsDto,) async {
|
||||||
final response = await restoreAssetsWithHttpInfo(bulkIdsDto,);
|
final response = await restoreAssetsWithHttpInfo(bulkIdsDto,);
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /trash/restore' operation and returns the [Response].
|
/// Performs an HTTP 'POST /trash/restore' operation and returns the [Response].
|
||||||
@@ -114,10 +130,18 @@ class TrashApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreTrash() async {
|
Future<TrashResponseDto?> restoreTrash() async {
|
||||||
final response = await restoreTrashWithHttpInfo();
|
final response = await restoreTrashWithHttpInfo();
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user