Merge branch 'main' of https://github.com/immich-app/immich into feat/combobox-positioning
This commit is contained in:
4
.github/workflows/static_analysis.yml
vendored
4
.github/workflows/static_analysis.yml
vendored
@@ -56,6 +56,10 @@ jobs:
|
||||
run: dart format lib/ --set-exit-if-changed
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run dart custom_lint
|
||||
run: dart run custom_lint
|
||||
working-directory: ./mobile
|
||||
|
||||
# Enable after riverpod generator migration is completed
|
||||
# - name: Run dart custom lint
|
||||
# run: dart run custom_lint
|
||||
|
||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -5,8 +5,8 @@
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"restart": true,
|
||||
"port": 9230,
|
||||
"name": "Immich Server",
|
||||
"port": 9231,
|
||||
"name": "Immich API Server",
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"localRoot": "${workspaceFolder}/server"
|
||||
},
|
||||
@@ -14,8 +14,8 @@
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"restart": true,
|
||||
"port": 9231,
|
||||
"name": "Immich Microservices",
|
||||
"port": 9230,
|
||||
"name": "Immich Workers",
|
||||
"remoteRoot": "/usr/src/app",
|
||||
"localRoot": "${workspaceFolder}/server"
|
||||
}
|
||||
|
||||
496
cli/package-lock.json
generated
496
cli/package-lock.json
generated
@@ -825,9 +825,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
|
||||
"integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
|
||||
"version": "9.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
|
||||
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -844,6 +844,19 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
@@ -1340,17 +1353,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
|
||||
"integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
|
||||
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.3.0",
|
||||
"@typescript-eslint/type-utils": "8.3.0",
|
||||
"@typescript-eslint/utils": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
||||
"@typescript-eslint/scope-manager": "8.5.0",
|
||||
"@typescript-eslint/type-utils": "8.5.0",
|
||||
"@typescript-eslint/utils": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1374,16 +1387,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
|
||||
"integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
|
||||
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.3.0",
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/typescript-estree": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
||||
"@typescript-eslint/scope-manager": "8.5.0",
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/typescript-estree": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1403,14 +1416,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
|
||||
"integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
|
||||
"integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0"
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1421,14 +1434,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
|
||||
"integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz",
|
||||
"integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.3.0",
|
||||
"@typescript-eslint/utils": "8.3.0",
|
||||
"@typescript-eslint/typescript-estree": "8.5.0",
|
||||
"@typescript-eslint/utils": "8.5.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1446,9 +1459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
|
||||
"integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
|
||||
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1460,14 +1473,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
|
||||
"integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
|
||||
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1489,16 +1502,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
|
||||
"integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
|
||||
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.3.0",
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/typescript-estree": "8.3.0"
|
||||
"@typescript-eslint/scope-manager": "8.5.0",
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/typescript-estree": "8.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1512,13 +1525,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
|
||||
"integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
|
||||
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1530,19 +1543,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
|
||||
"integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
|
||||
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@bcoe/v8-coverage": "^0.2.3",
|
||||
"debug": "^4.3.5",
|
||||
"debug": "^4.3.6",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.6",
|
||||
"istanbul-reports": "^3.1.7",
|
||||
"magic-string": "^0.30.10",
|
||||
"magic-string": "^0.30.11",
|
||||
"magicast": "^0.3.4",
|
||||
"std-env": "^3.7.0",
|
||||
"test-exclude": "^7.0.1",
|
||||
@@ -1552,17 +1566,24 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "2.0.5"
|
||||
"@vitest/browser": "2.1.0",
|
||||
"vitest": "2.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
|
||||
"integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
|
||||
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "2.0.5",
|
||||
"@vitest/utils": "2.0.5",
|
||||
"@vitest/spy": "2.1.0",
|
||||
"@vitest/utils": "2.1.0",
|
||||
"chai": "^5.1.1",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
@@ -1570,11 +1591,40 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
|
||||
"integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
|
||||
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
|
||||
"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": {
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
@@ -1583,12 +1633,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
|
||||
"integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
|
||||
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "2.0.5",
|
||||
"@vitest/utils": "2.1.0",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1596,24 +1647,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
|
||||
"integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
|
||||
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.0.5",
|
||||
"magic-string": "^0.30.10",
|
||||
"@vitest/pretty-format": "2.1.0",
|
||||
"magic-string": "^0.30.11",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
|
||||
"integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
|
||||
"node_modules/@vitest/snapshot/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/@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": {
|
||||
"tinyspy": "^3.0.0"
|
||||
},
|
||||
@@ -1622,13 +1688,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
|
||||
"integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
|
||||
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.0.5",
|
||||
"estree-walker": "^3.0.3",
|
||||
"@vitest/pretty-format": "2.1.0",
|
||||
"loupe": "^3.1.1",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
@@ -1636,6 +1702,19 @@
|
||||
"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": {
|
||||
"version": "8.12.1",
|
||||
"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",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -1800,6 +1880,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1838,6 +1919,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
|
||||
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
@@ -1870,6 +1952,7 @@
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
@@ -2014,6 +2097,7 @@
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -2112,9 +2196,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.9.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
|
||||
"integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
|
||||
"version": "9.10.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
|
||||
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2122,7 +2206,8 @@
|
||||
"@eslint-community/regexpp": "^4.11.0",
|
||||
"@eslint/config-array": "^0.18.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/retry": "^0.3.0",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
@@ -2145,7 +2230,6 @@
|
||||
"is-glob": "^4.0.0",
|
||||
"is-path-inside": "^3.0.3",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"levn": "^0.4.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -2383,6 +2467,7 @@
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
@@ -2396,29 +2481,6 @@
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"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",
|
||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
@@ -2688,15 +2739,6 @@
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"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": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||
@@ -2818,18 +2860,6 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"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",
|
||||
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-func-name": "^2.0.1"
|
||||
}
|
||||
@@ -3061,12 +3092,6 @@
|
||||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -3087,18 +3112,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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
@@ -3219,48 +3232,6 @@
|
||||
"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": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||
@@ -3397,21 +3368,23 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
||||
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -3436,9 +3409,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3457,8 +3430,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -3827,10 +3800,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3933,18 +3907,6 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
@@ -4031,10 +3993,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
|
||||
"integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
|
||||
"dev": true
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
@@ -4055,10 +4025,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
|
||||
"integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
|
||||
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@@ -4140,9 +4111,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -4210,14 +4181,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
||||
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4270,15 +4241,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
|
||||
"integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz",
|
||||
"integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.3.5",
|
||||
"debug": "^4.3.6",
|
||||
"pathe": "^1.1.2",
|
||||
"tinyrainbow": "^1.2.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4312,29 +4283,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
|
||||
"integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz",
|
||||
"integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@vitest/expect": "2.0.5",
|
||||
"@vitest/pretty-format": "^2.0.5",
|
||||
"@vitest/runner": "2.0.5",
|
||||
"@vitest/snapshot": "2.0.5",
|
||||
"@vitest/spy": "2.0.5",
|
||||
"@vitest/utils": "2.0.5",
|
||||
"@vitest/expect": "2.1.0",
|
||||
"@vitest/mocker": "2.1.0",
|
||||
"@vitest/pretty-format": "^2.1.0",
|
||||
"@vitest/runner": "2.1.0",
|
||||
"@vitest/snapshot": "2.1.0",
|
||||
"@vitest/spy": "2.1.0",
|
||||
"@vitest/utils": "2.1.0",
|
||||
"chai": "^5.1.1",
|
||||
"debug": "^4.3.5",
|
||||
"execa": "^8.0.1",
|
||||
"magic-string": "^0.30.10",
|
||||
"debug": "^4.3.6",
|
||||
"magic-string": "^0.30.11",
|
||||
"pathe": "^1.1.2",
|
||||
"std-env": "^3.7.0",
|
||||
"tinybench": "^2.8.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.0",
|
||||
"tinypool": "^1.0.0",
|
||||
"tinyrainbow": "^1.2.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "2.0.5",
|
||||
"vite-node": "2.1.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4349,8 +4321,8 @@
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "2.0.5",
|
||||
"@vitest/ui": "2.0.5",
|
||||
"@vitest/browser": "2.1.0",
|
||||
"@vitest/ui": "2.1.0",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -4535,9 +4507,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
||||
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
||||
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
||||
@@ -43,6 +43,7 @@ services:
|
||||
ports:
|
||||
- 3001:3001
|
||||
- 9230:9230
|
||||
- 9231:9231
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
|
||||
34
docs/package-lock.json
generated
34
docs/package-lock.json
generated
@@ -6068,9 +6068,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-lunr-search": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.4.0.tgz",
|
||||
"integrity": "sha512-GfllnNXCLgTSPH9TAKWmbn8VMfwpdOAZ1xl3T2GgX8Pm26qSDLfrrdVwjguaLfMJfzciFL97RKrAJlgrFM48yw==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.5.0.tgz",
|
||||
"integrity": "sha512-k3zN4jYMi/prWInJILGKOxE+BVcgYinwj9+gcECsYm52tS+4ZKzXQzbPnVJAEXmvKOfFMcDFvS3MSmm6cEaxIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"autocomplete.js": "^0.37.0",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -6097,14 +6098,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-lunr-search/node_modules/@types/unist": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
|
||||
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/docusaurus-lunr-search/node_modules/bail": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
|
||||
"integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
@@ -6114,6 +6117,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -6122,6 +6126,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -6130,6 +6135,7 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
|
||||
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
@@ -6139,6 +6145,7 @@
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz",
|
||||
"integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bail": "^1.0.0",
|
||||
"extend": "^3.0.0",
|
||||
@@ -6156,6 +6163,7 @@
|
||||
"version": "2.0.3",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2.0.2"
|
||||
},
|
||||
@@ -6168,6 +6176,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz",
|
||||
"integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2.0.0",
|
||||
"is-buffer": "^2.0.0",
|
||||
@@ -6183,6 +6192,7 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
|
||||
"integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2.0.0",
|
||||
"unist-util-stringify-position": "^2.0.0"
|
||||
@@ -16081,9 +16091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
|
||||
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
|
||||
"version": "3.4.11",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
|
||||
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -16443,9 +16453,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
727
e2e/package-lock.json
generated
727
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,11 @@ describe('/trash', () => {
|
||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||
|
||||
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
const { status, body } = await request(app)
|
||||
.post('/trash/empty')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ count: 1 });
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
||||
|
||||
@@ -51,8 +54,11 @@ describe('/trash', () => {
|
||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
|
||||
|
||||
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
const { status, body } = await request(app)
|
||||
.post('/trash/empty')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ count: 1 });
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
|
||||
|
||||
@@ -76,8 +82,11 @@ describe('/trash', () => {
|
||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||
|
||||
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
const { status, body } = await request(app)
|
||||
.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) });
|
||||
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
|
||||
@@ -99,11 +108,12 @@ describe('/trash', () => {
|
||||
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(before.isTrashed).toBe(true);
|
||||
|
||||
const { status } = await request(app)
|
||||
const { status, body } = await request(app)
|
||||
.post('/trash/restore/assets')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ids: [assetId] });
|
||||
expect(status).toBe(204);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ count: 1 });
|
||||
|
||||
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(after.isTrashed).toBe(false);
|
||||
|
||||
@@ -94,6 +94,7 @@ export const signupResponseDto = {
|
||||
quotaSizeInBytes: null,
|
||||
status: 'active',
|
||||
license: null,
|
||||
profileChangedAt: expect.any(String),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -36,8 +36,31 @@ analyzer:
|
||||
- openapi/**
|
||||
- lib/generated_plugin_registrant.dart
|
||||
|
||||
plugins:
|
||||
- custom_lint
|
||||
plugins:
|
||||
- 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
|
||||
- photo_manager:
|
||||
exclude:
|
||||
# required / wanted
|
||||
- album_media.repository.dart
|
||||
- asset_media.repository.dart
|
||||
- file_media.repository.dart
|
||||
# acceptable exceptions for the time being
|
||||
- asset.entity.dart # to provide local AssetEntity for now
|
||||
- immich_local_image_provider.dart # accesses thumbnails via PhotoManager
|
||||
- immich_local_thumbnail_provider.dart # accesses thumbnails via PhotoManager
|
||||
# refactor to make the providers and services testable
|
||||
- backup.provider.dart # uses only PMProgressHandler
|
||||
- manual_upload.provider.dart # uses only PMProgressHandler
|
||||
- background.service.dart # uses only PMProgressHandler
|
||||
- backup.service.dart # uses only PMProgressHandler
|
||||
|
||||
dart_code_metrics:
|
||||
metrics:
|
||||
|
||||
1
mobile/immich_lint/analysis_options.yaml
Normal file
1
mobile/immich_lint/analysis_options.yaml
Normal file
@@ -0,0 +1 @@
|
||||
include: package:lints/recommended.yaml
|
||||
49
mobile/immich_lint/lib/immich_mobile_immich_lint.dart
Normal file
49
mobile/immich_lint/lib/immich_mobile_immich_lint.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:analyzer/error/error.dart' show ErrorSeverity;
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
|
||||
PluginBase createPlugin() => ImmichLinter();
|
||||
|
||||
class ImmichLinter extends PluginBase {
|
||||
@override
|
||||
List<LintRule> getLintRules(CustomLintConfigs configs) => [
|
||||
PhotoManagerRule(configs.rules[PhotoManagerRule._code.name]),
|
||||
];
|
||||
}
|
||||
|
||||
class PhotoManagerRule extends DartLintRule {
|
||||
PhotoManagerRule(LintOptions? options) : super(code: _code) {
|
||||
final excludeOption = options?.json["exclude"];
|
||||
if (excludeOption is String) {
|
||||
_excludePaths.add(excludeOption);
|
||||
} else if (excludeOption is List) {
|
||||
_excludePaths.addAll(excludeOption.map((option) => option));
|
||||
}
|
||||
}
|
||||
|
||||
final Set<String> _excludePaths = HashSet();
|
||||
|
||||
static const _code = LintCode(
|
||||
name: 'photo_manager',
|
||||
problemMessage:
|
||||
'photo_manager library must only be used in MediaRepository',
|
||||
errorSeverity: ErrorSeverity.WARNING,
|
||||
);
|
||||
|
||||
@override
|
||||
void run(
|
||||
CustomLintResolver resolver,
|
||||
ErrorReporter reporter,
|
||||
CustomLintContext context,
|
||||
) {
|
||||
if (_excludePaths.contains(resolver.source.shortName)) return;
|
||||
|
||||
context.registry.addImportDirective((node) {
|
||||
if (node.uri.stringValue?.startsWith("package:photo_manager") == true) {
|
||||
reporter.atNode(node, code);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
370
mobile/immich_lint/pubspec.lock
Normal file
370
mobile/immich_lint/pubspec.lock
Normal file
@@ -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: transitive
|
||||
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"
|
||||
13
mobile/immich_lint/pubspec.yaml
Normal file
13
mobile/immich_lint/pubspec.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^4.0.0
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'package:flutter/foundation.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/utils/datetime_comparison.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
part 'album.entity.g.dart';
|
||||
|
||||
@@ -43,6 +41,9 @@ class Album {
|
||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||
|
||||
@ignore
|
||||
bool isAll = false;
|
||||
|
||||
@ignore
|
||||
bool get isRemote => remoteId != null;
|
||||
|
||||
@@ -70,6 +71,9 @@ class Album {
|
||||
return name.join(' ');
|
||||
}
|
||||
|
||||
@ignore
|
||||
String get eTagKeyAssetCount => "device-album-$localId-asset-count";
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Album) return false;
|
||||
@@ -112,19 +116,6 @@ class Album {
|
||||
sharedUsers.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 {
|
||||
final Isar db = Isar.getInstance()!;
|
||||
final Album a = Album(
|
||||
@@ -177,7 +168,3 @@ extension AssetsHelper on IsarCollection<Album> {
|
||||
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 '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:isar/isar.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:path/path.dart' as p;
|
||||
|
||||
@@ -42,33 +41,6 @@ class Asset {
|
||||
stackId = remote.stack?.id,
|
||||
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({
|
||||
this.id = Isar.autoIncrement,
|
||||
required this.checksum,
|
||||
@@ -115,6 +87,8 @@ class Asset {
|
||||
return _local;
|
||||
}
|
||||
|
||||
set local(AssetEntity? assetEntity) => _local = assetEntity;
|
||||
|
||||
Id id = Isar.autoIncrement;
|
||||
|
||||
/// stores the raw SHA1 bytes as a base64 String
|
||||
@@ -210,6 +184,10 @@ class Asset {
|
||||
@ignore
|
||||
Duration get duration => Duration(seconds: durationInSeconds);
|
||||
|
||||
// ignore: invalid_annotation_target
|
||||
@ignore
|
||||
set byteHash(List<int> hash) => checksum = base64.encode(hash);
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Asset) return false;
|
||||
|
||||
21
mobile/lib/interfaces/album_media.interface.dart
Normal file
21
mobile/lib/interfaces/album_media.interface.dart
Normal file
@@ -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);
|
||||
}
|
||||
7
mobile/lib/interfaces/asset_media.interface.dart
Normal file
7
mobile/lib/interfaces/asset_media.interface.dart
Normal file
@@ -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);
|
||||
}
|
||||
30
mobile/lib/interfaces/file_media.interface.dart
Normal file
30
mobile/lib/interfaces/file_media.interface.dart
Normal file
@@ -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();
|
||||
}
|
||||
@@ -1,45 +1,47 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
|
||||
class AvailableAlbum {
|
||||
final AssetPathEntity albumEntity;
|
||||
final Album album;
|
||||
final int assetCount;
|
||||
final DateTime? lastBackup;
|
||||
AvailableAlbum({
|
||||
required this.albumEntity,
|
||||
required this.album,
|
||||
required this.assetCount,
|
||||
this.lastBackup,
|
||||
});
|
||||
|
||||
AvailableAlbum copyWith({
|
||||
AssetPathEntity? albumEntity,
|
||||
Album? album,
|
||||
int? assetCount,
|
||||
DateTime? lastBackup,
|
||||
Uint8List? thumbnailData,
|
||||
}) {
|
||||
return AvailableAlbum(
|
||||
albumEntity: albumEntity ?? this.albumEntity,
|
||||
album: album ?? this.album,
|
||||
assetCount: assetCount ?? this.assetCount,
|
||||
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 => albumEntity.isAll;
|
||||
bool get isAll => album.isAll;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
|
||||
'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is AvailableAlbum && other.albumEntity == albumEntity;
|
||||
return other is AvailableAlbum && other.album == album;
|
||||
}
|
||||
|
||||
@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 {
|
||||
BackupCandidate({required this.asset, required this.albumNames});
|
||||
|
||||
AssetEntity asset;
|
||||
Asset asset;
|
||||
List<String> albumNames;
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
|
||||
class ErrorUploadAsset {
|
||||
final String id;
|
||||
final DateTime fileCreatedAt;
|
||||
final String fileName;
|
||||
final String fileType;
|
||||
final AssetEntity asset;
|
||||
final Asset asset;
|
||||
final String errorMessage;
|
||||
|
||||
const ErrorUploadAsset({
|
||||
@@ -22,7 +22,7 @@ class ErrorUploadAsset {
|
||||
DateTime? fileCreatedAt,
|
||||
String? fileName,
|
||||
String? fileType,
|
||||
AssetEntity? asset,
|
||||
Asset? asset,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return ErrorUploadAsset(
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.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/theme_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AlbumPreviewPage extends HookConsumerWidget {
|
||||
final AssetPathEntity album;
|
||||
final Album album;
|
||||
const AlbumPreviewPage({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assets = useState<List<AssetEntity>>([]);
|
||||
final assets = useState<List<Asset>>([]);
|
||||
|
||||
getAssetsInAlbum() async {
|
||||
assets.value = await album.getAssetListRange(
|
||||
start: 0,
|
||||
end: await album.assetCountAsync,
|
||||
);
|
||||
assets.value = await ref
|
||||
.read(albumMediaRepositoryProvider)
|
||||
.getAssets(album.localId!);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
@@ -68,30 +67,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
||||
),
|
||||
itemCount: assets.value.length,
|
||||
itemBuilder: (context, index) {
|
||||
Future<Uint8List?> thumbData =
|
||||
assets.value[index].thumbnailDataWithSize(
|
||||
const ThumbnailSize(200, 200),
|
||||
quality: 50,
|
||||
);
|
||||
|
||||
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(),
|
||||
);
|
||||
}),
|
||||
return ImmichThumbnail(
|
||||
asset: assets.value[index],
|
||||
width: 100,
|
||||
height: 100,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/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:photo_manager/photo_manager.dart';
|
||||
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class FailedBackupStatusPage extends HookConsumerWidget {
|
||||
@@ -70,11 +69,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Image(
|
||||
fit: BoxFit.cover,
|
||||
image: AssetEntityImageProvider(
|
||||
errorAsset.asset,
|
||||
isOriginal: false,
|
||||
thumbnailSize: const ThumbnailSize.square(512),
|
||||
thumbnailFormat: ThumbnailFormat.jpeg,
|
||||
image: ImmichLocalThumbnailProvider(
|
||||
asset: errorAsset.asset,
|
||||
height: 512,
|
||||
width: 512,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,11 +8,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.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_toast.dart';
|
||||
import 'package:auto_route/auto_route.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:easy_localization/easy_localization.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -67,10 +67,10 @@ class EditImagePage extends ConsumerWidget {
|
||||
) async {
|
||||
try {
|
||||
final Uint8List imageData = await _imageToUint8List(image);
|
||||
await PhotoManager.editor.saveImage(
|
||||
imageData,
|
||||
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
||||
);
|
||||
await ref.read(fileMediaRepositoryProvider).saveImage(
|
||||
imageData,
|
||||
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
||||
);
|
||||
await ref.read(albumProvider.notifier).getDeviceAlbums();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
ImmichToast.show(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/entities/exif_info.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:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AssetNotifier extends StateNotifier<bool> {
|
||||
final AssetService _assetService;
|
||||
@@ -257,7 +257,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
// Delete asset from device
|
||||
if (local.isNotEmpty) {
|
||||
try {
|
||||
return await PhotoManager.editor.deleteWithIds(local);
|
||||
return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
|
||||
} catch (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/widgets.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/entities/backup_album.entity.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/success_upload_asset.model.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/backup.service.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:logging/logging.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> {
|
||||
BackupNotifier(
|
||||
@@ -38,6 +43,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
this._backgroundService,
|
||||
this._galleryPermissionNotifier,
|
||||
this._db,
|
||||
this._albumMediaRepository,
|
||||
this._fileMediaRepository,
|
||||
this.ref,
|
||||
) : super(
|
||||
BackUpState(
|
||||
@@ -86,6 +93,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
final BackgroundService _backgroundService;
|
||||
final GalleryPermissionNotifier _galleryPermissionNotifier;
|
||||
final Isar _db;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
final Ref ref;
|
||||
|
||||
///
|
||||
@@ -224,22 +233,24 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
Stopwatch stopwatch = Stopwatch()..start();
|
||||
// Get all albums on the device
|
||||
List<AvailableAlbum> availableAlbums = [];
|
||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
type: RequestType.common,
|
||||
);
|
||||
List<Album> albums = await _albumMediaRepository.getAll();
|
||||
|
||||
// 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');
|
||||
|
||||
for (AssetPathEntity album in albums) {
|
||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||
for (Album album in albums) {
|
||||
AvailableAlbum availableAlbum = AvailableAlbum(
|
||||
album: album,
|
||||
assetCount: await ref
|
||||
.read(albumMediaRepositoryProvider)
|
||||
.getAssetCount(album.localId!),
|
||||
);
|
||||
|
||||
availableAlbums.add(availableAlbum);
|
||||
|
||||
albumMap[album.id] = album;
|
||||
albumMap[album.localId!] = album;
|
||||
}
|
||||
state = state.copyWith(availableAlbums: availableAlbums);
|
||||
|
||||
@@ -248,14 +259,18 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
final List<BackupAlbum> selectedBackupAlbums =
|
||||
await _backupService.selectedAlbumsQuery().findAll();
|
||||
|
||||
// Generate AssetPathEntity from id to add to local state
|
||||
final Set<AvailableAlbum> selectedAlbums = {};
|
||||
for (final BackupAlbum ba in selectedBackupAlbums) {
|
||||
final albumAsset = albumMap[ba.id];
|
||||
|
||||
if (albumAsset != null) {
|
||||
selectedAlbums.add(
|
||||
AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
|
||||
AvailableAlbum(
|
||||
album: albumAsset,
|
||||
assetCount:
|
||||
await _albumMediaRepository.getAssetCount(albumAsset.localId!),
|
||||
lastBackup: ba.lastBackup,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
log.severe('Selected album not found');
|
||||
@@ -268,7 +283,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
if (albumAsset != null) {
|
||||
excludedAlbums.add(
|
||||
AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
|
||||
AvailableAlbum(
|
||||
album: albumAsset,
|
||||
assetCount: await ref
|
||||
.read(albumMediaRepositoryProvider)
|
||||
.getAssetCount(albumAsset.localId!),
|
||||
lastBackup: ba.lastBackup,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
log.severe('Excluded album not found');
|
||||
@@ -297,23 +318,24 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
final Set<BackupCandidate> assetsFromExcludedAlbums = {};
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final assets = await album.albumEntity.getAssetListRange(
|
||||
start: 0,
|
||||
end: assetCount,
|
||||
);
|
||||
final assets = await ref
|
||||
.read(albumMediaRepositoryProvider)
|
||||
.getAssets(album.album.localId!);
|
||||
|
||||
// Add album's name to the asset info
|
||||
for (final asset in assets) {
|
||||
List<String> albumNames = [album.name];
|
||||
|
||||
final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull(
|
||||
(a) => a.asset.id == asset.id,
|
||||
(a) => a.asset.localId == asset.localId,
|
||||
);
|
||||
|
||||
if (existingAsset != null) {
|
||||
@@ -331,16 +353,17 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
}
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final assets = await album.albumEntity.getAssetListRange(
|
||||
start: 0,
|
||||
end: assetCount,
|
||||
);
|
||||
final assets = await ref
|
||||
.read(albumMediaRepositoryProvider)
|
||||
.getAssets(album.album.localId!);
|
||||
|
||||
for (final asset in assets) {
|
||||
assetsFromExcludedAlbums.add(
|
||||
@@ -360,14 +383,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
// Find asset that were backup from selected albums
|
||||
final Set<String> selectedAlbumsBackupAssets =
|
||||
Set.from(allUniqueAssets.map((e) => e.asset.id));
|
||||
Set.from(allUniqueAssets.map((e) => e.asset.localId));
|
||||
|
||||
selectedAlbumsBackupAssets
|
||||
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
||||
|
||||
// Remove duplicated asset from all unique assets
|
||||
allUniqueAssets.removeWhere(
|
||||
(candidate) => duplicatedAssetIds.contains(candidate.asset.id),
|
||||
(candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
|
||||
);
|
||||
|
||||
if (allUniqueAssets.isEmpty) {
|
||||
@@ -454,7 +477,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
final hasPermission = _galleryPermissionNotifier.hasPermission;
|
||||
if (hasPermission) {
|
||||
await PhotoManager.clearFileCache();
|
||||
await _fileMediaRepository.clearFileCache();
|
||||
|
||||
if (state.allUniqueAssets.isEmpty) {
|
||||
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);
|
||||
// Remove item that has already been backed up
|
||||
for (final assetId in state.allAssetsInDatabase) {
|
||||
assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId);
|
||||
assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId);
|
||||
}
|
||||
|
||||
if (assetsWillBeBackup.isEmpty) {
|
||||
@@ -531,7 +554,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
state = state.copyWith(
|
||||
allUniqueAssets: state.allUniqueAssets
|
||||
.where(
|
||||
(candidate) => candidate.asset.id != result.candidate.asset.id,
|
||||
(candidate) =>
|
||||
candidate.asset.localId != result.candidate.asset.localId,
|
||||
)
|
||||
.toSet(),
|
||||
);
|
||||
@@ -539,11 +563,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
state = state.copyWith(
|
||||
selectedAlbumsBackupAssetsIds: {
|
||||
...state.selectedAlbumsBackupAssetsIds,
|
||||
result.candidate.asset.id,
|
||||
result.candidate.asset.localId!,
|
||||
},
|
||||
allAssetsInDatabase: [
|
||||
...state.allAssetsInDatabase,
|
||||
result.candidate.asset.id,
|
||||
result.candidate.asset.localId!,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -552,7 +576,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
state.selectedAlbumsBackupAssetsIds.length ==
|
||||
0) {
|
||||
final latestAssetBackup = state.allUniqueAssets
|
||||
.map((candidate) => candidate.asset.modifiedDateTime)
|
||||
.map((candidate) => candidate.asset.fileModifiedAt)
|
||||
.reduce(
|
||||
(v, e) => e.isAfter(v) ? e : v,
|
||||
);
|
||||
@@ -741,6 +765,8 @@ final backupProvider =
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(galleryPermissionNotifier.notifier),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/repositories/file_media.repository.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/current_upload_asset.model.dart';
|
||||
@@ -27,7 +28,7 @@ import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.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 =
|
||||
StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
|
||||
@@ -193,17 +194,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
|
||||
|
||||
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
|
||||
// where platform specific fields such as `subtype` used to detect platform specific assets such as
|
||||
// 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()),
|
||||
);
|
||||
final allAssetsFromDevice =
|
||||
allManualUploads.where((e) => e.isLocal && !e.isRemote).toList();
|
||||
|
||||
if (allAssetsFromDevice.length != allManualUploads.length) {
|
||||
_log.warning(
|
||||
@@ -221,11 +215,17 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
await _backupService.buildUploadCandidates(
|
||||
selectedBackupAlbums,
|
||||
excludedBackupAlbums,
|
||||
useTimeFilter: false,
|
||||
);
|
||||
|
||||
// Extrack candidate from allAssetsFromDevice.nonNulls
|
||||
final uploadAssets = candidates
|
||||
.where((e) => allAssetsFromDevice.nonNulls.contains(e.asset));
|
||||
// Extrack candidate from allAssetsFromDevice
|
||||
final uploadAssets = candidates.where(
|
||||
(candidate) =>
|
||||
allAssetsFromDevice.firstWhereOrNull(
|
||||
(asset) => asset.localId == candidate.asset.localId,
|
||||
) !=
|
||||
null,
|
||||
);
|
||||
|
||||
if (uploadAssets.isEmpty) {
|
||||
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/store.entity.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
|
||||
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/painting.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
|
||||
/// Only viable
|
||||
|
||||
93
mobile/lib/repositories/album_media.repository.dart
Normal file
93
mobile/lib/repositories/album_media.repository.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
46
mobile/lib/repositories/asset_media.repository.dart
Normal file
46
mobile/lib/repositories/asset_media.repository.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
62
mobile/lib/repositories/file_media.repository.dart
Normal file
62
mobile/lib/repositories/file_media.repository.dart
Normal file
@@ -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();
|
||||
}
|
||||
@@ -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:isar/isar.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' hide LatLng;
|
||||
|
||||
part 'router.gr.dart';
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ class AlbumOptionsRouteArgs {
|
||||
class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
|
||||
AlbumPreviewRoute({
|
||||
Key? key,
|
||||
required AssetPathEntity album,
|
||||
required Album album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumPreviewRoute.name,
|
||||
@@ -218,7 +218,7 @@ class AlbumPreviewRouteArgs {
|
||||
|
||||
final Key? key;
|
||||
|
||||
final AssetPathEntity album;
|
||||
final Album album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/album.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/interfaces/user.interface.dart';
|
||||
@@ -19,13 +20,13 @@ import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album.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/user.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final albumServiceProvider = Provider(
|
||||
(ref) => AlbumService(
|
||||
@@ -36,6 +37,7 @@ final albumServiceProvider = Provider(
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,6 +49,7 @@ class AlbumService {
|
||||
final IAssetRepository _assetRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final IBackupRepository _backupAlbumRepository;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final Logger _log = Logger('AlbumService');
|
||||
Completer<bool> _localCompleter = Completer()..complete(false);
|
||||
Completer<bool> _remoteCompleter = Completer()..complete(false);
|
||||
@@ -59,6 +62,7 @@ class AlbumService {
|
||||
this._assetRepository,
|
||||
this._userRepository,
|
||||
this._backupAlbumRepository,
|
||||
this._albumMediaRepository,
|
||||
);
|
||||
|
||||
/// Checks all selected device albums for changes of albums and their assets
|
||||
@@ -84,11 +88,7 @@ class AlbumService {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final List<AssetPathEntity> onDevice =
|
||||
await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
filterOption: FilterOptionGroup(containsPathModified: true),
|
||||
);
|
||||
final List<Album> onDevice = await _albumMediaRepository.getAll();
|
||||
_log.info("Found ${onDevice.length} device albums");
|
||||
Set<String>? excludedAssets;
|
||||
if (excludedIds.isNotEmpty) {
|
||||
@@ -104,13 +104,15 @@ class AlbumService {
|
||||
_log.info("Found ${excludedAssets.length} assets to exclude");
|
||||
}
|
||||
// remove all excluded albums
|
||||
onDevice.removeWhere((e) => excludedIds.contains(e.id));
|
||||
onDevice.removeWhere((e) => excludedIds.contains(e.localId));
|
||||
_log.info(
|
||||
"Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
|
||||
);
|
||||
}
|
||||
final hasAll = selectedIds
|
||||
.map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
|
||||
.map(
|
||||
(id) => onDevice.firstWhereOrNull((album) => album.localId == id),
|
||||
)
|
||||
.whereNotNull()
|
||||
.any((a) => a.isAll);
|
||||
if (hasAll) {
|
||||
@@ -122,7 +124,7 @@ class AlbumService {
|
||||
}
|
||||
} else {
|
||||
// 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");
|
||||
}
|
||||
changes =
|
||||
@@ -136,15 +138,15 @@ class AlbumService {
|
||||
}
|
||||
|
||||
Future<Set<String>> _loadExcludedAssetIds(
|
||||
List<AssetPathEntity> albums,
|
||||
List<Album> albums,
|
||||
List<String> excludedAlbumIds,
|
||||
) async {
|
||||
final Set<String> result = HashSet<String>();
|
||||
for (AssetPathEntity a in albums) {
|
||||
if (excludedAlbumIds.contains(a.id)) {
|
||||
final List<AssetEntity> assets =
|
||||
await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
|
||||
result.addAll(assets.map((e) => e.id));
|
||||
for (Album album in albums) {
|
||||
if (excludedAlbumIds.contains(album.localId)) {
|
||||
final assetIds =
|
||||
await _albumMediaRepository.getAssetIds(album.localId!);
|
||||
result.addAll(assetIds);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -321,7 +321,7 @@ class AssetService {
|
||||
|
||||
for (BackupCandidate candidate in candidates) {
|
||||
final asset = remoteAssets.firstWhereOrNull(
|
||||
(a) => a.localId == candidate.asset.id,
|
||||
(a) => a.localId == candidate.asset.localId,
|
||||
);
|
||||
|
||||
if (asset != null) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/repositories/album.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/hash.service.dart';
|
||||
@@ -34,7 +36,7 @@ import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:isar/isar.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(
|
||||
(ref) => BackgroundService(),
|
||||
@@ -363,8 +365,10 @@ class BackgroundService {
|
||||
AssetRepository assetRepository = AssetRepository(db);
|
||||
UserRepository userRepository = UserRepository(db);
|
||||
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||
HashService hashService = HashService(db, this);
|
||||
SyncService syncSerive = SyncService(db, hashService);
|
||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||
HashService hashService = HashService(db, this, albumMediaRepository);
|
||||
SyncService syncSerive = SyncService(db, hashService, albumMediaRepository);
|
||||
UserService userService =
|
||||
UserService(apiService, db, syncSerive, partnerService);
|
||||
AlbumService albumService = AlbumService(
|
||||
@@ -375,9 +379,16 @@ class BackgroundService {
|
||||
assetRepository,
|
||||
userRepository,
|
||||
backupAlbumRepository,
|
||||
albumMediaRepository,
|
||||
);
|
||||
BackupService backupService = BackupService(
|
||||
apiService,
|
||||
db,
|
||||
settingService,
|
||||
albumService,
|
||||
albumMediaRepository,
|
||||
fileMediaRepository,
|
||||
);
|
||||
BackupService backupService =
|
||||
BackupService(apiService, db, settingService, albumService);
|
||||
|
||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
||||
@@ -385,7 +396,7 @@ class BackgroundService {
|
||||
return true;
|
||||
}
|
||||
|
||||
await PhotoManager.setIgnorePermissionCheck(true);
|
||||
await fileMediaRepository.enableBackgroundAccess();
|
||||
|
||||
do {
|
||||
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:flutter/material.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/duplicated_asset.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/current_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/app_settings.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/api.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:path/path.dart' as p;
|
||||
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(
|
||||
(ref) => BackupService(
|
||||
@@ -32,6 +38,8 @@ final backupServiceProvider = Provider(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -42,12 +50,16 @@ class BackupService {
|
||||
final Logger _log = Logger("BackupService");
|
||||
final AppSettingsService _appSetting;
|
||||
final AlbumService _albumService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
|
||||
BackupService(
|
||||
this._apiService,
|
||||
this._db,
|
||||
this._appSetting,
|
||||
this._albumService,
|
||||
this._albumMediaRepository,
|
||||
this._fileMediaRepository,
|
||||
);
|
||||
|
||||
Future<List<String>?> getDeviceBackupAsset() async {
|
||||
@@ -86,44 +98,17 @@ class BackupService {
|
||||
List<BackupAlbum> excludedBackupAlbums, {
|
||||
bool useTimeFilter = true,
|
||||
}) 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 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(
|
||||
selectedAlbums,
|
||||
selectedBackupAlbums,
|
||||
now,
|
||||
useTimeFilter: useTimeFilter,
|
||||
);
|
||||
|
||||
if (toAdd.isEmpty) return {};
|
||||
|
||||
final Set<BackupCandidate> toRemove = await _fetchAssetsAndUpdateLastBackup(
|
||||
excludedAlbums,
|
||||
excludedBackupAlbums,
|
||||
now,
|
||||
useTimeFilter: useTimeFilter,
|
||||
@@ -132,92 +117,62 @@ class BackupService {
|
||||
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(
|
||||
List<AssetPathEntity?> localAlbums,
|
||||
List<BackupAlbum> backupAlbums,
|
||||
DateTime now, {
|
||||
bool useTimeFilter = true,
|
||||
}) async {
|
||||
Set<BackupCandidate> candidate = {};
|
||||
Set<BackupCandidate> candidates = {};
|
||||
|
||||
for (int i = 0; i < localAlbums.length; i++) {
|
||||
final localAlbum = localAlbums[i];
|
||||
if (localAlbum == null) {
|
||||
for (final BackupAlbum backupAlbum in backupAlbums) {
|
||||
final Album localAlbum;
|
||||
try {
|
||||
localAlbum = await _albumMediaRepository.get(backupAlbum.id);
|
||||
} on StateError {
|
||||
// the album no longer exists
|
||||
continue;
|
||||
}
|
||||
|
||||
if (useTimeFilter &&
|
||||
localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) ==
|
||||
true) {
|
||||
localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) {
|
||||
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;
|
||||
}
|
||||
|
||||
final assets = await localAlbum.getAssetListRange(
|
||||
start: 0,
|
||||
end: await localAlbum.assetCountAsync,
|
||||
);
|
||||
|
||||
// Add album's name to the asset info
|
||||
for (final asset in assets) {
|
||||
List<String> albumNames = [localAlbum.name];
|
||||
|
||||
final existingAsset = candidate.firstWhereOrNull(
|
||||
(a) => a.asset.id == asset.id,
|
||||
final existingAsset = candidates.firstWhereOrNull(
|
||||
(candidate) => candidate.asset.localId == asset.localId,
|
||||
);
|
||||
|
||||
if (existingAsset != null) {
|
||||
albumNames.addAll(existingAsset.albumNames);
|
||||
candidate.remove(existingAsset);
|
||||
candidates.remove(existingAsset);
|
||||
}
|
||||
|
||||
candidate.add(
|
||||
BackupCandidate(
|
||||
asset: asset,
|
||||
albumNames: albumNames,
|
||||
),
|
||||
);
|
||||
candidates.add(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
|
||||
@@ -230,7 +185,7 @@ class BackupService {
|
||||
|
||||
final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
|
||||
candidates.removeWhere(
|
||||
(candidate) => duplicatedAssetIds.contains(candidate.asset.id),
|
||||
(candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
|
||||
);
|
||||
|
||||
if (candidates.isEmpty) {
|
||||
@@ -243,7 +198,7 @@ class BackupService {
|
||||
final CheckExistingAssetsResponseDto? duplicates =
|
||||
await _apiService.assetsApi.checkExistingAssets(
|
||||
CheckExistingAssetsDto(
|
||||
deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
|
||||
deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(),
|
||||
deviceId: deviceId,
|
||||
),
|
||||
);
|
||||
@@ -259,7 +214,7 @@ class BackupService {
|
||||
}
|
||||
|
||||
if (existing.isNotEmpty) {
|
||||
candidates.removeWhere((c) => existing.contains(c.asset.id));
|
||||
candidates.removeWhere((c) => existing.contains(c.asset.localId));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
@@ -278,7 +233,7 @@ class BackupService {
|
||||
|
||||
// DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
|
||||
if (Platform.isIOS) {
|
||||
await PhotoManager.requestPermissionExtend();
|
||||
await _fileMediaRepository.requestExtendedPermissions();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -289,9 +244,9 @@ class BackupService {
|
||||
List<BackupCandidate> _sortPhotosFirst(List<BackupCandidate> candidates) {
|
||||
return candidates.sorted(
|
||||
(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;
|
||||
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) {
|
||||
final AssetEntity entity = candidate.asset;
|
||||
final Asset asset = candidate.asset;
|
||||
File? file;
|
||||
File? livePhotoFile;
|
||||
|
||||
try {
|
||||
final isAvailableLocally =
|
||||
await entity.isLocallyAvailable(isOrigin: true);
|
||||
await asset.local!.isLocallyAvailable(isOrigin: true);
|
||||
|
||||
// Handle getting files from iCloud
|
||||
if (!isAvailableLocally && Platform.isIOS) {
|
||||
@@ -342,39 +297,41 @@ class BackupService {
|
||||
|
||||
onCurrentAsset(
|
||||
CurrentUploadAsset(
|
||||
id: entity.id,
|
||||
fileCreatedAt: entity.createDateTime.year == 1970
|
||||
? entity.modifiedDateTime
|
||||
: entity.createDateTime,
|
||||
fileName: await entity.titleAsync,
|
||||
fileType: _getAssetType(entity.type),
|
||||
id: asset.localId!,
|
||||
fileCreatedAt: asset.fileCreatedAt.year == 1970
|
||||
? asset.fileModifiedAt
|
||||
: asset.fileCreatedAt,
|
||||
fileName: asset.fileName,
|
||||
fileType: _getAssetType(asset.type),
|
||||
iCloudAsset: true,
|
||||
),
|
||||
);
|
||||
|
||||
file = await entity.loadFile(progressHandler: pmProgressHandler);
|
||||
if (entity.isLivePhoto) {
|
||||
livePhotoFile = await entity.loadFile(
|
||||
file =
|
||||
await asset.local!.loadFile(progressHandler: pmProgressHandler);
|
||||
if (asset.local!.isLivePhoto) {
|
||||
livePhotoFile = await asset.local!.loadFile(
|
||||
withSubtype: true,
|
||||
progressHandler: pmProgressHandler,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (entity.type == AssetType.video) {
|
||||
file = await entity.originFile;
|
||||
if (asset.type == AssetType.video) {
|
||||
file = await asset.local!.originFile;
|
||||
} else {
|
||||
file = await entity.originFile.timeout(const Duration(seconds: 5));
|
||||
if (entity.isLivePhoto) {
|
||||
livePhotoFile = await entity.originFileWithSubtype
|
||||
file = await asset.local!.originFile
|
||||
.timeout(const Duration(seconds: 5));
|
||||
if (asset.local!.isLivePhoto) {
|
||||
livePhotoFile = await asset.local!.originFileWithSubtype
|
||||
.timeout(const Duration(seconds: 5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
String originalFileName = await entity.titleAsync;
|
||||
String originalFileName = asset.fileName;
|
||||
|
||||
if (entity.isLivePhoto) {
|
||||
if (asset.local!.isLivePhoto) {
|
||||
if (livePhotoFile == null) {
|
||||
_log.warning(
|
||||
"Failed to obtain motion part of the livePhoto - $originalFileName",
|
||||
@@ -398,31 +355,31 @@ class BackupService {
|
||||
|
||||
baseRequest.headers.addAll(ApiService.getRequestHeaders());
|
||||
baseRequest.headers["Transfer-Encoding"] = "chunked";
|
||||
baseRequest.fields['deviceAssetId'] = entity.id;
|
||||
baseRequest.fields['deviceAssetId'] = asset.localId!;
|
||||
baseRequest.fields['deviceId'] = deviceId;
|
||||
baseRequest.fields['fileCreatedAt'] =
|
||||
entity.createDateTime.toUtc().toIso8601String();
|
||||
asset.fileCreatedAt.toUtc().toIso8601String();
|
||||
baseRequest.fields['fileModifiedAt'] =
|
||||
entity.modifiedDateTime.toUtc().toIso8601String();
|
||||
baseRequest.fields['isFavorite'] = entity.isFavorite.toString();
|
||||
baseRequest.fields['duration'] = entity.videoDuration.toString();
|
||||
asset.fileModifiedAt.toUtc().toIso8601String();
|
||||
baseRequest.fields['isFavorite'] = asset.isFavorite.toString();
|
||||
baseRequest.fields['duration'] = asset.duration.toString();
|
||||
baseRequest.files.add(assetRawUploadData);
|
||||
|
||||
onCurrentAsset(
|
||||
CurrentUploadAsset(
|
||||
id: entity.id,
|
||||
fileCreatedAt: entity.createDateTime.year == 1970
|
||||
? entity.modifiedDateTime
|
||||
: entity.createDateTime,
|
||||
id: asset.localId!,
|
||||
fileCreatedAt: asset.fileCreatedAt.year == 1970
|
||||
? asset.fileModifiedAt
|
||||
: asset.fileCreatedAt,
|
||||
fileName: originalFileName,
|
||||
fileType: _getAssetType(entity.type),
|
||||
fileType: _getAssetType(asset.type),
|
||||
fileSize: file.lengthSync(),
|
||||
iCloudAsset: false,
|
||||
),
|
||||
);
|
||||
|
||||
String? livePhotoVideoId;
|
||||
if (entity.isLivePhoto && livePhotoFile != null) {
|
||||
if (asset.local!.isLivePhoto && livePhotoFile != null) {
|
||||
livePhotoVideoId = await uploadLivePhotoVideo(
|
||||
originalFileName,
|
||||
livePhotoFile,
|
||||
@@ -448,16 +405,16 @@ class BackupService {
|
||||
final errorMessage = error['message'] ?? error['error'];
|
||||
|
||||
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(
|
||||
ErrorUploadAsset(
|
||||
asset: entity,
|
||||
id: entity.id,
|
||||
fileCreatedAt: entity.createDateTime,
|
||||
asset: asset,
|
||||
id: asset.localId!,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileName: originalFileName,
|
||||
fileType: _getAssetType(entity.type),
|
||||
fileType: _getAssetType(candidate.asset.type),
|
||||
errorMessage: errorMessage,
|
||||
),
|
||||
);
|
||||
@@ -473,7 +430,7 @@ class BackupService {
|
||||
bool isDuplicate = false;
|
||||
if (response.statusCode == 200) {
|
||||
isDuplicate = true;
|
||||
duplicatedAssetIds.add(entity.id);
|
||||
duplicatedAssetIds.add(asset.localId!);
|
||||
}
|
||||
|
||||
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/exif_info.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/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PhotoManager;
|
||||
|
||||
/// Finds duplicates originating from missing EXIF information
|
||||
class BackupVerificationService {
|
||||
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
|
||||
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
|
||||
@@ -71,6 +73,7 @@ class BackupVerificationService {
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
),
|
||||
);
|
||||
final upper = compute(
|
||||
@@ -81,6 +84,7 @@ class BackupVerificationService {
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
),
|
||||
);
|
||||
toDelete = await lower + await upper;
|
||||
@@ -93,6 +97,7 @@ class BackupVerificationService {
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -106,12 +111,13 @@ class BackupVerificationService {
|
||||
String auth,
|
||||
String endpoint,
|
||||
RootIsolateToken rootIsolateToken,
|
||||
IFileMediaRepository fileMediaRepository,
|
||||
}) tuple,
|
||||
) async {
|
||||
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
||||
final List<Asset> result = [];
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
||||
await PhotoManager.setIgnorePermissionCheck(true);
|
||||
await tuple.fileMediaRepository.enableBackgroundAccess();
|
||||
final ApiService apiService = ApiService();
|
||||
apiService.setEndpoint(tuple.endpoint);
|
||||
apiService.setAccessToken(tuple.auth);
|
||||
@@ -228,5 +234,6 @@ class BackupVerificationService {
|
||||
final backupVerificationServiceProvider = Provider(
|
||||
(ref) => BackupVerificationService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.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/entities/android_device_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:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class HashService {
|
||||
HashService(this._db, this._backgroundService);
|
||||
HashService(this._db, this._backgroundService, this._albumMediaRepository);
|
||||
final Isar _db;
|
||||
final BackgroundService _backgroundService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final _log = Logger('HashService');
|
||||
|
||||
/// Returns all assets that were successfully hashed
|
||||
Future<List<Asset>> getHashedAssets(
|
||||
AssetPathEntity album, {
|
||||
Album album, {
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
DateTime? modifiedFrom,
|
||||
DateTime? modifiedUntil,
|
||||
Set<String>? excludedAssets,
|
||||
}) 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
|
||||
? entities
|
||||
: entities.where((e) => !excludedAssets.contains(e.id)).toList();
|
||||
: entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
|
||||
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
|
||||
/// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
|
||||
/// 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 batchDataSize = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
final ids = assetEntities
|
||||
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
||||
final ids = assets
|
||||
.map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
|
||||
.toList();
|
||||
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
||||
final List<DeviceAsset> toAdd = [];
|
||||
@@ -50,22 +61,16 @@ class HashService {
|
||||
|
||||
int bytes = 0;
|
||||
|
||||
for (int i = 0; i < assetEntities.length; i++) {
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
if (hashes[i] != null) {
|
||||
continue;
|
||||
}
|
||||
final file = await assetEntities[i].originFile;
|
||||
final file = await assets[i].local!.originFile;
|
||||
if (file == null) {
|
||||
final fileName = await assetEntities[i].titleAsync.catchError((error) {
|
||||
_log.warning(
|
||||
"Failed to get title for asset ${assetEntities[i].id}",
|
||||
);
|
||||
|
||||
return "";
|
||||
});
|
||||
final fileName = assets[i].fileName;
|
||||
|
||||
_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;
|
||||
}
|
||||
@@ -86,7 +91,7 @@ class HashService {
|
||||
if (toHash.isNotEmpty) {
|
||||
await _processBatch(toHash, toAdd);
|
||||
}
|
||||
return _mapAllHashedAssets(assetEntities, hashes);
|
||||
return _getHashedAssets(assets, hashes);
|
||||
}
|
||||
|
||||
/// Lookup hashes of assets by their local ID
|
||||
@@ -133,15 +138,16 @@ class HashService {
|
||||
return hashes;
|
||||
}
|
||||
|
||||
/// Converts [AssetEntity]s that were successfully hashed to [Asset]s
|
||||
List<Asset> _mapAllHashedAssets(
|
||||
List<AssetEntity> assets,
|
||||
/// Returns all successfully hashed [Asset]s with their hash value set
|
||||
List<Asset> _getHashedAssets(
|
||||
List<Asset> assets,
|
||||
List<DeviceAsset?> hashes,
|
||||
) {
|
||||
final List<Asset> result = [];
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
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;
|
||||
@@ -152,5 +158,6 @@ final hashServiceProvider = Provider(
|
||||
(ref) => HashService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,21 +3,27 @@ import 'dart:io';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.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/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
final imageViewerServiceProvider =
|
||||
Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
|
||||
final imageViewerServiceProvider = Provider(
|
||||
(ref) => ImageViewerService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class ImageViewerService {
|
||||
final ApiService _apiService;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
final Logger _log = Logger("ImageViewerService");
|
||||
|
||||
ImageViewerService(this._apiService);
|
||||
ImageViewerService(this._apiService, this._fileMediaRepository);
|
||||
|
||||
Future<bool> downloadAsset(Asset asset) async {
|
||||
File? imageFile;
|
||||
@@ -46,7 +52,7 @@ class ImageViewerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
AssetEntity? entity;
|
||||
Asset? resultAsset;
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
videoFile = await File('${tempDir.path}/livephoto.mov').create();
|
||||
@@ -54,24 +60,21 @@ class ImageViewerService {
|
||||
videoFile.writeAsBytesSync(motionResponse.bodyBytes);
|
||||
imageFile.writeAsBytesSync(imageResponse.bodyBytes);
|
||||
|
||||
entity = await PhotoManager.editor.darwin.saveLivePhoto(
|
||||
imageFile: imageFile,
|
||||
videoFile: videoFile,
|
||||
resultAsset = await _fileMediaRepository.saveLivePhoto(
|
||||
image: imageFile,
|
||||
video: videoFile,
|
||||
title: asset.fileName,
|
||||
);
|
||||
|
||||
if (entity == null) {
|
||||
if (resultAsset == null) {
|
||||
_log.warning(
|
||||
"Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
|
||||
);
|
||||
|
||||
entity = await PhotoManager.editor.saveImage(
|
||||
imageResponse.bodyBytes,
|
||||
title: asset.fileName,
|
||||
);
|
||||
resultAsset = await _fileMediaRepository
|
||||
.saveImage(imageResponse.bodyBytes, title: asset.fileName);
|
||||
}
|
||||
|
||||
return entity != null;
|
||||
return resultAsset != null;
|
||||
} else {
|
||||
var res = await _apiService.assetsApi
|
||||
.downloadAssetWithHttpInfo(asset.remoteId!);
|
||||
@@ -81,11 +84,11 @@ class ImageViewerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
final AssetEntity? entity;
|
||||
final Asset? resultAsset;
|
||||
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
|
||||
|
||||
if (asset.isImage) {
|
||||
entity = await PhotoManager.editor.saveImage(
|
||||
resultAsset = await _fileMediaRepository.saveImage(
|
||||
res.bodyBytes,
|
||||
title: asset.fileName,
|
||||
relativePath: relativePath,
|
||||
@@ -94,13 +97,13 @@ class ImageViewerService {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
|
||||
videoFile.writeAsBytesSync(res.bodyBytes);
|
||||
entity = await PhotoManager.editor.saveVideo(
|
||||
resultAsset = await _fileMediaRepository.saveVideo(
|
||||
videoFile,
|
||||
title: asset.fileName,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
}
|
||||
return entity != null;
|
||||
return resultAsset != null;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error saving downloaded asset", error, stack);
|
||||
|
||||
@@ -8,7 +8,9 @@ import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
@@ -17,19 +19,23 @@ import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final syncServiceProvider = Provider(
|
||||
(ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)),
|
||||
(ref) => SyncService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(hashServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class SyncService {
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
|
||||
SyncService(this._db, this._hashService);
|
||||
SyncService(this._db, this._hashService, this._albumMediaRepository);
|
||||
|
||||
// public methods:
|
||||
|
||||
@@ -68,7 +74,7 @@ class SyncService {
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> syncLocalAlbumAssetsToDb(
|
||||
List<AssetPathEntity> onDevice, [
|
||||
List<Album> onDevice, [
|
||||
Set<String>? excludedAssets,
|
||||
]) =>
|
||||
_lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets));
|
||||
@@ -492,7 +498,7 @@ class SyncService {
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> _syncLocalAlbumAssetsToDb(
|
||||
List<AssetPathEntity> onDevice, [
|
||||
List<Album> onDevice, [
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||
@@ -504,16 +510,15 @@ class SyncService {
|
||||
final bool anyChanges = await diffSortedLists(
|
||||
onDevice,
|
||||
inDb,
|
||||
compare: (AssetPathEntity a, Album b) => a.id.compareTo(b.localId!),
|
||||
both: (AssetPathEntity ape, Album album) => _syncAlbumInDbAndOnDevice(
|
||||
ape,
|
||||
album,
|
||||
compare: (Album a, Album b) => a.localId!.compareTo(b.localId!),
|
||||
both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(
|
||||
a,
|
||||
b,
|
||||
deleteCandidates,
|
||||
existing,
|
||||
excludedAssets,
|
||||
),
|
||||
onlyFirst: (AssetPathEntity ape) =>
|
||||
_addAlbumFromDevice(ape, existing, excludedAssets),
|
||||
onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets),
|
||||
onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
|
||||
);
|
||||
_log.fine(
|
||||
@@ -541,58 +546,65 @@ class SyncService {
|
||||
/// returns `true` if there were any changes
|
||||
/// Accumulates asset candidates to delete and those already existing in DB
|
||||
Future<bool> _syncAlbumInDbAndOnDevice(
|
||||
AssetPathEntity ape,
|
||||
Album album,
|
||||
Album deviceAlbum,
|
||||
Album dbAlbum,
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing, [
|
||||
Set<String>? excludedAssets,
|
||||
bool forceRefresh = false,
|
||||
]) async {
|
||||
if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) {
|
||||
_log.fine("Local album ${ape.name} has not changed. Skipping sync.");
|
||||
if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) {
|
||||
_log.fine(
|
||||
"Local album ${deviceAlbum.name} has not changed. Skipping sync.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!forceRefresh &&
|
||||
excludedAssets == null &&
|
||||
await _syncDeviceAlbumFast(ape, album)) {
|
||||
await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice = await ape.assetCountAsync;
|
||||
final List<Asset> onDevice =
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
final int assetCountOnDevice =
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
final List<Asset> onDevice = await _hashService.getHashedAssets(
|
||||
deviceAlbum,
|
||||
excludedAssets: excludedAssets,
|
||||
);
|
||||
_removeDuplicates(onDevice);
|
||||
// _removeDuplicates sorts `onDevice` by checksum
|
||||
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
||||
if (toAdd.isEmpty &&
|
||||
toUpdate.isEmpty &&
|
||||
toDelete.isEmpty &&
|
||||
album.name == ape.name &&
|
||||
ape.lastModified != null &&
|
||||
album.modifiedAt.isAtSameMomentAs(ape.lastModified!)) {
|
||||
dbAlbum.name == deviceAlbum.name &&
|
||||
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
|
||||
// changes only affeted excluded albums
|
||||
_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 !=
|
||||
_db.eTags.getByIdSync(ape.eTagKeyAssetCount)?.assetCount) {
|
||||
_db.eTags.getByIdSync(deviceAlbum.eTagKeyAssetCount)?.assetCount) {
|
||||
await _db.writeTxn(
|
||||
() => _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_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);
|
||||
_log.fine(
|
||||
@@ -600,28 +612,31 @@ class SyncService {
|
||||
);
|
||||
deleteCandidates.addAll(toDelete);
|
||||
existing.addAll(existingInDb);
|
||||
album.name = ape.name;
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
if (album.thumbnail.value != null &&
|
||||
toDelete.contains(album.thumbnail.value)) {
|
||||
album.thumbnail.value = null;
|
||||
dbAlbum.name = deviceAlbum.name;
|
||||
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
||||
if (dbAlbum.thumbnail.value != null &&
|
||||
toDelete.contains(dbAlbum.thumbnail.value)) {
|
||||
dbAlbum.thumbnail.value = null;
|
||||
}
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await album.assets
|
||||
await dbAlbum.assets
|
||||
.update(link: existingInDb + updated, unlink: toDelete);
|
||||
await _db.albums.put(album);
|
||||
album.thumbnail.value ??= await album.assets.filter().findFirst();
|
||||
await album.thumbnail.save();
|
||||
await _db.albums.put(dbAlbum);
|
||||
dbAlbum.thumbnail.value ??= await dbAlbum.assets.filter().findFirst();
|
||||
await dbAlbum.thumbnail.save();
|
||||
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) {
|
||||
_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;
|
||||
@@ -629,45 +644,45 @@ class SyncService {
|
||||
|
||||
/// fast path for common case: only new assets were added to device album
|
||||
/// returns `true` if successfull, else `false`
|
||||
Future<bool> _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async {
|
||||
if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
|
||||
Future<bool> _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async {
|
||||
if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) {
|
||||
return false;
|
||||
}
|
||||
final int totalOnDevice = await ape.assetCountAsync;
|
||||
final int totalOnDevice =
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
final int lastKnownTotal =
|
||||
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0;
|
||||
final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
|
||||
? await ape.fetchPathProperties(
|
||||
filterOptionGroup: FilterOptionGroup(
|
||||
updateTimeCond: DateTimeCond(
|
||||
min: album.modifiedAt.add(const Duration(seconds: 1)),
|
||||
max: ape.lastModified ?? DateTime.now(),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
if (modified == null) {
|
||||
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ??
|
||||
0;
|
||||
if (totalOnDevice <= lastKnownTotal) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
||||
_removeDuplicates(newAssets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await album.assets.update(link: existingInDb + updated);
|
||||
await _db.albums.put(album);
|
||||
await _db.eTags
|
||||
.put(ETag(id: ape.eTagKeyAssetCount, assetCount: totalOnDevice));
|
||||
await dbAlbum.assets.update(link: existingInDb + updated);
|
||||
await _db.albums.put(dbAlbum);
|
||||
await _db.eTags.put(
|
||||
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) {
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -677,14 +692,15 @@ class SyncService {
|
||||
/// 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
|
||||
Future<void> _addAlbumFromDevice(
|
||||
AssetPathEntity ape,
|
||||
Album album,
|
||||
List<Asset> existing, [
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
_log.info("Syncing a new local album to DB: ${ape.name}");
|
||||
final Album a = Album.local(ape);
|
||||
final assets =
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
_log.info("Syncing a new local album to DB: ${album.name}");
|
||||
final assets = await _hashService.getHashedAssets(
|
||||
album,
|
||||
excludedAssets: excludedAssets,
|
||||
);
|
||||
_removeDuplicates(assets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||
_log.info(
|
||||
@@ -692,15 +708,15 @@ class SyncService {
|
||||
);
|
||||
await upsertAssetsWithExif(updated);
|
||||
existing.addAll(existingInDb);
|
||||
a.assets.addAll(existingInDb);
|
||||
a.assets.addAll(updated);
|
||||
album.assets.addAll(existingInDb);
|
||||
album.assets.addAll(updated);
|
||||
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
|
||||
a.thumbnail.value = thumb;
|
||||
album.thumbnail.value = thumb;
|
||||
try {
|
||||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
_log.info("Added a new local album to DB: ${ape.name}");
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
_log.info("Added a new local album to DB: ${album.name}");
|
||||
} 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 +814,15 @@ class SyncService {
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
|
||||
return a.name != b.name ||
|
||||
a.lastModified == null ||
|
||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
|
||||
await a.assetCountAsync !=
|
||||
(await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
|
||||
Future<bool> _hasAlbumChangeOnDevice(
|
||||
Album deviceAlbum,
|
||||
Album dbAlbum,
|
||||
) async {
|
||||
return deviceAlbum.name != dbAlbum.name ||
|
||||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
|
||||
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
?.assetCount;
|
||||
}
|
||||
|
||||
Future<bool> _removeAllLocalAlbumsAndAssets() async {
|
||||
|
||||
@@ -12,6 +12,16 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
addDefault(value, 'tags', TagsResponse().toJson());
|
||||
}
|
||||
break;
|
||||
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: const EdgeInsets.only(top: 2.0),
|
||||
child: FutureBuilder(
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
snapshot.data.toString() +
|
||||
(album.isAll
|
||||
? " (${'backup_all'.tr()})"
|
||||
: ""),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text("0");
|
||||
}),
|
||||
future: album.assetCount,
|
||||
child: Text(
|
||||
album.assetCount.toString() +
|
||||
(album.isAll ? " (${'backup_all'.tr()})" : ""),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -208,7 +198,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
AlbumPreviewRoute(album: album.albumEntity),
|
||||
AlbumPreviewRoute(album: album.album),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
@@ -24,19 +23,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
|
||||
final bool isExcluded =
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
|
||||
final assetCount = useState(0);
|
||||
final syncAlbum = ref
|
||||
.watch(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.syncAlbums);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
album.assetCount.then((value) => assetCount.value = value);
|
||||
return null;
|
||||
},
|
||||
[album],
|
||||
);
|
||||
|
||||
buildTileColor() {
|
||||
if (isSelected) {
|
||||
return context.isDarkTheme
|
||||
@@ -117,11 +107,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(assetCount.value.toString()),
|
||||
subtitle: Text(album.assetCount.toString()),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
AlbumPreviewRoute(album: album.albumEntity),
|
||||
AlbumPreviewRoute(album: album.album),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
|
||||
@@ -2,18 +2,19 @@ import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.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/theme_extensions.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/error_backup_list.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:photo_manager/photo_manager.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
|
||||
class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
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() {
|
||||
if (asset.iCloudAsset != null && asset.iCloudAsset!) {
|
||||
return Padding(
|
||||
@@ -239,8 +229,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder<Uint8List?>(
|
||||
future: buildAssetThumbnail(),
|
||||
return FutureBuilder<Asset?>(
|
||||
future: ref.read(assetMediaRepositoryProvider).get(asset.id),
|
||||
builder: (context, thumbnail) => ListTile(
|
||||
isThreeLine: true,
|
||||
leading: AnimatedCrossFade(
|
||||
@@ -250,9 +240,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
child: thumbnail.hasData
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Image.memory(
|
||||
thumbnail.data!,
|
||||
fit: BoxFit.cover,
|
||||
child: ImmichThumbnail(
|
||||
asset: thumbnail.data,
|
||||
width: 50,
|
||||
height: 50,
|
||||
),
|
||||
|
||||
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@@ -452,6 +452,7 @@ Class | Method | HTTP request | Description
|
||||
- [ToneMapping](doc//ToneMapping.md)
|
||||
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
|
||||
- [TranscodePolicy](doc//TranscodePolicy.md)
|
||||
- [TrashResponseDto](doc//TrashResponseDto.md)
|
||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -265,6 +265,7 @@ part 'model/time_bucket_size.dart';
|
||||
part 'model/tone_mapping.dart';
|
||||
part 'model/transcode_hw_accel.dart';
|
||||
part 'model/transcode_policy.dart';
|
||||
part 'model/trash_response_dto.dart';
|
||||
part 'model/update_album_dto.dart';
|
||||
part 'model/update_album_user_dto.dart';
|
||||
part 'model/update_asset_dto.dart';
|
||||
|
||||
30
mobile/openapi/lib/api/trash_api.dart
generated
30
mobile/openapi/lib/api/trash_api.dart
generated
@@ -42,11 +42,19 @@ class TrashApi {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> emptyTrash() async {
|
||||
Future<TrashResponseDto?> emptyTrash() async {
|
||||
final response = await emptyTrashWithHttpInfo();
|
||||
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), 'TrashResponseDto',) as TrashResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /trash/restore/assets' operation and returns the [Response].
|
||||
@@ -81,11 +89,19 @@ class TrashApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [BulkIdsDto] bulkIdsDto (required):
|
||||
Future<void> restoreAssets(BulkIdsDto bulkIdsDto,) async {
|
||||
Future<TrashResponseDto?> restoreAssets(BulkIdsDto bulkIdsDto,) async {
|
||||
final response = await restoreAssetsWithHttpInfo(bulkIdsDto,);
|
||||
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), 'TrashResponseDto',) as TrashResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 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();
|
||||
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), 'TrashResponseDto',) as TrashResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -585,6 +585,8 @@ class ApiClient {
|
||||
return TranscodeHWAccelTypeTransformer().decode(value);
|
||||
case 'TranscodePolicy':
|
||||
return TranscodePolicyTypeTransformer().decode(value);
|
||||
case 'TrashResponseDto':
|
||||
return TrashResponseDto.fromJson(value);
|
||||
case 'UpdateAlbumDto':
|
||||
return UpdateAlbumDto.fromJson(value);
|
||||
case 'UpdateAlbumUserDto':
|
||||
|
||||
@@ -13,30 +13,36 @@ part of openapi.api;
|
||||
class CreateProfileImageResponseDto {
|
||||
/// Returns a new [CreateProfileImageResponseDto] instance.
|
||||
CreateProfileImageResponseDto({
|
||||
required this.profileChangedAt,
|
||||
required this.profileImagePath,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
DateTime profileChangedAt;
|
||||
|
||||
String profileImagePath;
|
||||
|
||||
String userId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is CreateProfileImageResponseDto &&
|
||||
other.profileChangedAt == profileChangedAt &&
|
||||
other.profileImagePath == profileImagePath &&
|
||||
other.userId == userId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(profileChangedAt.hashCode) +
|
||||
(profileImagePath.hashCode) +
|
||||
(userId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CreateProfileImageResponseDto[profileImagePath=$profileImagePath, userId=$userId]';
|
||||
String toString() => 'CreateProfileImageResponseDto[profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, userId=$userId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
json[r'userId'] = this.userId;
|
||||
return json;
|
||||
@@ -50,6 +56,7 @@ class CreateProfileImageResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return CreateProfileImageResponseDto(
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
);
|
||||
@@ -99,6 +106,7 @@ class CreateProfileImageResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'profileChangedAt',
|
||||
'profileImagePath',
|
||||
'userId',
|
||||
};
|
||||
|
||||
10
mobile/openapi/lib/model/partner_response_dto.dart
generated
10
mobile/openapi/lib/model/partner_response_dto.dart
generated
@@ -18,6 +18,7 @@ class PartnerResponseDto {
|
||||
required this.id,
|
||||
this.inTimeline,
|
||||
required this.name,
|
||||
required this.profileChangedAt,
|
||||
required this.profileImagePath,
|
||||
});
|
||||
|
||||
@@ -37,6 +38,8 @@ class PartnerResponseDto {
|
||||
|
||||
String name;
|
||||
|
||||
DateTime profileChangedAt;
|
||||
|
||||
String profileImagePath;
|
||||
|
||||
@override
|
||||
@@ -46,6 +49,7 @@ class PartnerResponseDto {
|
||||
other.id == id &&
|
||||
other.inTimeline == inTimeline &&
|
||||
other.name == name &&
|
||||
other.profileChangedAt == profileChangedAt &&
|
||||
other.profileImagePath == profileImagePath;
|
||||
|
||||
@override
|
||||
@@ -56,10 +60,11 @@ class PartnerResponseDto {
|
||||
(id.hashCode) +
|
||||
(inTimeline == null ? 0 : inTimeline!.hashCode) +
|
||||
(name.hashCode) +
|
||||
(profileChangedAt.hashCode) +
|
||||
(profileImagePath.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, email=$email, id=$id, inTimeline=$inTimeline, name=$name, profileImagePath=$profileImagePath]';
|
||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, email=$email, id=$id, inTimeline=$inTimeline, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -72,6 +77,7 @@ class PartnerResponseDto {
|
||||
// json[r'inTimeline'] = null;
|
||||
}
|
||||
json[r'name'] = this.name;
|
||||
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
return json;
|
||||
}
|
||||
@@ -89,6 +95,7 @@ class PartnerResponseDto {
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
);
|
||||
}
|
||||
@@ -141,6 +148,7 @@ class PartnerResponseDto {
|
||||
'email',
|
||||
'id',
|
||||
'name',
|
||||
'profileChangedAt',
|
||||
'profileImagePath',
|
||||
};
|
||||
}
|
||||
|
||||
98
mobile/openapi/lib/model/trash_response_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/trash_response_dto.dart
generated
Normal file
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class TrashResponseDto {
|
||||
/// Returns a new [TrashResponseDto] instance.
|
||||
TrashResponseDto({
|
||||
required this.count,
|
||||
});
|
||||
|
||||
int count;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is TrashResponseDto &&
|
||||
other.count == count;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(count.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'TrashResponseDto[count=$count]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'count'] = this.count;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [TrashResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static TrashResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return TrashResponseDto(
|
||||
count: mapValueOfType<int>(json, r'count')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<TrashResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <TrashResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = TrashResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, TrashResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, TrashResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = TrashResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of TrashResponseDto-objects as value to a dart map
|
||||
static Map<String, List<TrashResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<TrashResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = TrashResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'count',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class UserAdminResponseDto {
|
||||
required this.license,
|
||||
required this.name,
|
||||
required this.oauthId,
|
||||
required this.profileChangedAt,
|
||||
required this.profileImagePath,
|
||||
required this.quotaSizeInBytes,
|
||||
required this.quotaUsageInBytes,
|
||||
@@ -49,6 +50,8 @@ class UserAdminResponseDto {
|
||||
|
||||
String oauthId;
|
||||
|
||||
DateTime profileChangedAt;
|
||||
|
||||
String profileImagePath;
|
||||
|
||||
int? quotaSizeInBytes;
|
||||
@@ -74,6 +77,7 @@ class UserAdminResponseDto {
|
||||
other.license == license &&
|
||||
other.name == name &&
|
||||
other.oauthId == oauthId &&
|
||||
other.profileChangedAt == profileChangedAt &&
|
||||
other.profileImagePath == profileImagePath &&
|
||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||
other.quotaUsageInBytes == quotaUsageInBytes &&
|
||||
@@ -94,6 +98,7 @@ class UserAdminResponseDto {
|
||||
(license == null ? 0 : license!.hashCode) +
|
||||
(name.hashCode) +
|
||||
(oauthId.hashCode) +
|
||||
(profileChangedAt.hashCode) +
|
||||
(profileImagePath.hashCode) +
|
||||
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
||||
(quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) +
|
||||
@@ -103,7 +108,7 @@ class UserAdminResponseDto {
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -124,6 +129,7 @@ class UserAdminResponseDto {
|
||||
}
|
||||
json[r'name'] = this.name;
|
||||
json[r'oauthId'] = this.oauthId;
|
||||
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
if (this.quotaSizeInBytes != null) {
|
||||
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
|
||||
@@ -163,6 +169,7 @@ class UserAdminResponseDto {
|
||||
license: UserLicense.fromJson(json[r'license']),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
oauthId: mapValueOfType<String>(json, r'oauthId')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
|
||||
@@ -226,6 +233,7 @@ class UserAdminResponseDto {
|
||||
'license',
|
||||
'name',
|
||||
'oauthId',
|
||||
'profileChangedAt',
|
||||
'profileImagePath',
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
|
||||
10
mobile/openapi/lib/model/user_response_dto.dart
generated
10
mobile/openapi/lib/model/user_response_dto.dart
generated
@@ -17,6 +17,7 @@ class UserResponseDto {
|
||||
required this.email,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.profileChangedAt,
|
||||
required this.profileImagePath,
|
||||
});
|
||||
|
||||
@@ -28,6 +29,8 @@ class UserResponseDto {
|
||||
|
||||
String name;
|
||||
|
||||
DateTime profileChangedAt;
|
||||
|
||||
String profileImagePath;
|
||||
|
||||
@override
|
||||
@@ -36,6 +39,7 @@ class UserResponseDto {
|
||||
other.email == email &&
|
||||
other.id == id &&
|
||||
other.name == name &&
|
||||
other.profileChangedAt == profileChangedAt &&
|
||||
other.profileImagePath == profileImagePath;
|
||||
|
||||
@override
|
||||
@@ -45,10 +49,11 @@ class UserResponseDto {
|
||||
(email.hashCode) +
|
||||
(id.hashCode) +
|
||||
(name.hashCode) +
|
||||
(profileChangedAt.hashCode) +
|
||||
(profileImagePath.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]';
|
||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -56,6 +61,7 @@ class UserResponseDto {
|
||||
json[r'email'] = this.email;
|
||||
json[r'id'] = this.id;
|
||||
json[r'name'] = this.name;
|
||||
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
|
||||
json[r'profileImagePath'] = this.profileImagePath;
|
||||
return json;
|
||||
}
|
||||
@@ -72,6 +78,7 @@ class UserResponseDto {
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
);
|
||||
}
|
||||
@@ -124,6 +131,7 @@ class UserResponseDto {
|
||||
'email',
|
||||
'id',
|
||||
'name',
|
||||
'profileChangedAt',
|
||||
'profileImagePath',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -836,6 +836,13 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
immich_mobile_immich_lint:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
path: immich_lint
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
||||
@@ -94,10 +94,12 @@ dev_dependencies:
|
||||
isar_generator: ^3.1.0+1
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
custom_lint: ^0.6.0
|
||||
custom_lint: ^0.6.4
|
||||
riverpod_lint: ^2.3.7
|
||||
riverpod_generator: ^2.3.9
|
||||
mocktail: ^1.0.3
|
||||
immich_mobile_immich_lint:
|
||||
path: './immich_lint'
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockHashService extends Mock implements HashService {}
|
||||
|
||||
class MockCurrentUserProvider extends StateNotifier<User?>
|
||||
with Mock
|
||||
implements CurrentUserProvider {
|
||||
|
||||
@@ -7,8 +7,9 @@ import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../repository.mocks.dart';
|
||||
import '../../service.mocks.dart';
|
||||
import '../../test_utils.dart';
|
||||
import 'shared_mocks.dart';
|
||||
|
||||
void main() {
|
||||
Asset makeAsset({
|
||||
@@ -38,6 +39,8 @@ void main() {
|
||||
group('Test SyncService grouped', () {
|
||||
late final Isar db;
|
||||
final MockHashService hs = MockHashService();
|
||||
final MockAlbumMediaRepository albumMediaRepository =
|
||||
MockAlbumMediaRepository();
|
||||
final owner = User(
|
||||
id: "1",
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -67,7 +70,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
test('test inserting existing assets', () async {
|
||||
SyncService s = SyncService(db, hs);
|
||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||
@@ -85,7 +88,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('test inserting new assets', () async {
|
||||
SyncService s = SyncService(db, hs);
|
||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||
@@ -106,7 +109,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('test syncing duplicate assets', () async {
|
||||
SyncService s = SyncService(db, hs);
|
||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
makeAsset(checksum: "b", remoteId: "1-1"),
|
||||
@@ -154,7 +157,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('test efficient sync', () async {
|
||||
SyncService s = SyncService(db, hs);
|
||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
||||
final List<Asset> toUpsert = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"), // changed
|
||||
makeAsset(checksum: "f", remoteId: "0-2"), // new
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
@@ -11,3 +14,9 @@ class MockAssetRepository extends Mock implements IAssetRepository {}
|
||||
class MockUserRepository extends Mock implements IUserRepository {}
|
||||
|
||||
class MockBackupRepository extends Mock implements IBackupRepository {}
|
||||
|
||||
class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
|
||||
|
||||
class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
|
||||
|
||||
class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
@@ -8,3 +9,5 @@ class MockApiService extends Mock implements ApiService {}
|
||||
class MockUserService extends Mock implements UserService {}
|
||||
|
||||
class MockSyncService extends Mock implements SyncService {}
|
||||
|
||||
class MockHashService extends Mock implements HashService {}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import '../fixtures/album.stub.dart';
|
||||
import '../repository.mocks.dart';
|
||||
import '../service.mocks.dart';
|
||||
|
||||
@@ -14,6 +15,7 @@ void main() {
|
||||
late MockAssetRepository assetRepository;
|
||||
late MockUserRepository userRepository;
|
||||
late MockBackupRepository backupRepository;
|
||||
late MockAlbumMediaRepository albumMediaRepository;
|
||||
|
||||
setUp(() {
|
||||
apiService = MockApiService();
|
||||
@@ -23,6 +25,7 @@ void main() {
|
||||
assetRepository = MockAssetRepository();
|
||||
userRepository = MockUserRepository();
|
||||
backupRepository = MockBackupRepository();
|
||||
albumMediaRepository = MockAlbumMediaRepository();
|
||||
|
||||
sut = AlbumService(
|
||||
apiService,
|
||||
@@ -32,6 +35,7 @@ void main() {
|
||||
assetRepository,
|
||||
userRepository,
|
||||
backupRepository,
|
||||
albumMediaRepository,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -48,5 +52,22 @@ void main() {
|
||||
expect(result, false);
|
||||
verify(() => syncService.removeAllLocalAlbumsAndAssets());
|
||||
});
|
||||
|
||||
test('one selected albums, two on device', () async {
|
||||
when(() => backupRepository.getIdsBySelection(BackupSelection.exclude))
|
||||
.thenAnswer((_) async => []);
|
||||
when(() => backupRepository.getIdsBySelection(BackupSelection.select))
|
||||
.thenAnswer((_) async => [AlbumStub.oneAsset.localId!]);
|
||||
when(() => albumMediaRepository.getAll())
|
||||
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
|
||||
when(() => syncService.syncLocalAlbumAssetsToDb(any(), any()))
|
||||
.thenAnswer((_) async => true);
|
||||
final result = await sut.refreshDeviceAlbums();
|
||||
expect(result, true);
|
||||
verify(
|
||||
() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null),
|
||||
).called(1);
|
||||
verifyNoMoreInteractions(syncService);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6839,7 +6839,14 @@
|
||||
"operationId": "emptyTrash",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TrashResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -6864,7 +6871,14 @@
|
||||
"operationId": "restoreTrash",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TrashResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -6899,7 +6913,14 @@
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TrashResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -8779,6 +8800,10 @@
|
||||
},
|
||||
"CreateProfileImageResponseDto": {
|
||||
"properties": {
|
||||
"profileChangedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8787,6 +8812,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"profileChangedAt",
|
||||
"profileImagePath",
|
||||
"userId"
|
||||
],
|
||||
@@ -10015,6 +10041,10 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -10024,6 +10054,7 @@
|
||||
"email",
|
||||
"id",
|
||||
"name",
|
||||
"profileChangedAt",
|
||||
"profileImagePath"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -12244,6 +12275,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TrashResponseDto": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"count"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateAlbumDto": {
|
||||
"properties": {
|
||||
"albumName": {
|
||||
@@ -12454,6 +12496,10 @@
|
||||
"oauthId": {
|
||||
"type": "string"
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -12492,6 +12538,7 @@
|
||||
"license",
|
||||
"name",
|
||||
"oauthId",
|
||||
"profileChangedAt",
|
||||
"profileImagePath",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
@@ -12653,6 +12700,10 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -12662,6 +12713,7 @@
|
||||
"email",
|
||||
"id",
|
||||
"name",
|
||||
"profileChangedAt",
|
||||
"profileImagePath"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
6
open-api/typescript-sdk/package-lock.json
generated
6
open-api/typescript-sdk/package-lock.json
generated
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -19,6 +19,7 @@ export type UserResponseDto = {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
profileChangedAt: string;
|
||||
profileImagePath: string;
|
||||
};
|
||||
export type ActivityResponseDto = {
|
||||
@@ -53,6 +54,7 @@ export type UserAdminResponseDto = {
|
||||
license: (UserLicense) | null;
|
||||
name: string;
|
||||
oauthId: string;
|
||||
profileChangedAt: string;
|
||||
profileImagePath: string;
|
||||
quotaSizeInBytes: number | null;
|
||||
quotaUsageInBytes: number | null;
|
||||
@@ -669,6 +671,7 @@ export type PartnerResponseDto = {
|
||||
id: string;
|
||||
inTimeline?: boolean;
|
||||
name: string;
|
||||
profileChangedAt: string;
|
||||
profileImagePath: string;
|
||||
};
|
||||
export type UpdatePartnerDto = {
|
||||
@@ -1243,6 +1246,9 @@ export type TimeBucketResponseDto = {
|
||||
count: number;
|
||||
timeBucket: string;
|
||||
};
|
||||
export type TrashResponseDto = {
|
||||
count: number;
|
||||
};
|
||||
export type UserUpdateMeDto = {
|
||||
email?: string;
|
||||
name?: string;
|
||||
@@ -1252,6 +1258,7 @@ export type CreateProfileImageDto = {
|
||||
file: Blob;
|
||||
};
|
||||
export type CreateProfileImageResponseDto = {
|
||||
profileChangedAt: string;
|
||||
profileImagePath: string;
|
||||
userId: string;
|
||||
};
|
||||
@@ -3069,13 +3076,19 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
|
||||
}));
|
||||
}
|
||||
export function emptyTrash(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/trash/empty", {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: TrashResponseDto;
|
||||
}>("/trash/empty", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function restoreTrash(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/trash/restore", {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: TrashResponseDto;
|
||||
}>("/trash/restore", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
@@ -3083,7 +3096,10 @@ export function restoreTrash(opts?: Oazapfts.RequestOpts) {
|
||||
export function restoreAssets({ bulkIdsDto }: {
|
||||
bulkIdsDto: BulkIdsDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/trash/restore/assets", oazapfts.json({
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: TrashResponseDto;
|
||||
}>("/trash/restore/assets", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: bulkIdsDto
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240910@sha256:3fd455fe051bef63b1440753596e2afa34ff0513fe30aa71a5b76ebb2d751e9f AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240917@sha256:3d92952d37cd68f5bf641aa80e5cc034e0d11f3774147f5db8db93138cfa5b3b AS dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -41,7 +41,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240910@sha256:4e03fe801b74eede63e91d2d9bce3a7b05699f536c211391f2d82a83c1f63470
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240917@sha256:67a40250f03812fe1e6f6b6345a3c7b71b3a9f24c65ed4862e82be8b3e53d23a
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
3885
server/package-lock.json
generated
3885
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -46,11 +46,11 @@
|
||||
"@nestjs/swagger": "^7.1.8",
|
||||
"@nestjs/typeorm": "^10.0.0",
|
||||
"@nestjs/websockets": "^10.2.2",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.49.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
|
||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.53.0",
|
||||
"@opentelemetry/sdk-node": "^0.53.0",
|
||||
"@react-email/components": "^0.0.24",
|
||||
"@react-email/components": "^0.0.25",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { TrashResponseDto } from 'src/dtos/trash.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { TrashService } from 'src/services/trash.service';
|
||||
@@ -12,23 +13,23 @@ export class TrashController {
|
||||
constructor(private service: TrashService) {}
|
||||
|
||||
@Post('empty')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.ASSET_DELETE })
|
||||
emptyTrash(@Auth() auth: AuthDto): Promise<void> {
|
||||
emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
|
||||
return this.service.empty(auth);
|
||||
}
|
||||
|
||||
@Post('restore')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.ASSET_DELETE })
|
||||
restoreTrash(@Auth() auth: AuthDto): Promise<void> {
|
||||
restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
|
||||
return this.service.restore(auth);
|
||||
}
|
||||
|
||||
@Post('restore/assets')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ permission: Permission.ASSET_DELETE })
|
||||
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
|
||||
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> {
|
||||
return this.service.restoreAssets(auth, dto);
|
||||
}
|
||||
}
|
||||
|
||||
6
server/src/dtos/trash.dto.ts
Normal file
6
server/src/dtos/trash.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class TrashResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
count!: number;
|
||||
}
|
||||
@@ -8,12 +8,6 @@ export class CreateProfileImageDto {
|
||||
|
||||
export class CreateProfileImageResponseDto {
|
||||
userId!: string;
|
||||
profileChangedAt!: Date;
|
||||
profileImagePath!: string;
|
||||
}
|
||||
|
||||
export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto {
|
||||
return {
|
||||
userId,
|
||||
profileImagePath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export class UserResponseDto {
|
||||
profileImagePath!: string;
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
avatarColor!: UserAvatarColor;
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
|
||||
export class UserLicense {
|
||||
@@ -47,6 +48,7 @@ export const mapUser = (entity: UserEntity): UserResponseDto => {
|
||||
name: entity.name,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
avatarColor: getPreferences(entity).avatar.color,
|
||||
profileChangedAt: entity.profileChangedAt,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -70,6 +70,9 @@ export class AssetEntity {
|
||||
@Column()
|
||||
type!: AssetType;
|
||||
|
||||
@Column({ type: 'enum', enum: AssetStatus, default: AssetStatus.ACTIVE })
|
||||
status!: AssetStatus;
|
||||
|
||||
@Column()
|
||||
originalPath!: string;
|
||||
|
||||
|
||||
@@ -67,4 +67,7 @@ export class UserEntity {
|
||||
|
||||
@OneToMany(() => UserMetadataEntity, (metadata) => metadata.user)
|
||||
metadata!: UserMetadataEntity[];
|
||||
|
||||
@Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
|
||||
@@ -182,6 +182,12 @@ export enum UserStatus {
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
export enum AssetStatus {
|
||||
ACTIVE = 'active',
|
||||
TRASHED = 'trashed',
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
export enum SourceType {
|
||||
MACHINE_LEARNING = 'machine-learning',
|
||||
EXIF = 'exif',
|
||||
|
||||
@@ -16,18 +16,15 @@ export interface AlbumInfoOptions {
|
||||
|
||||
export interface IAlbumRepository extends IBulkAsset {
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
removeAsset(assetId: string): Promise<void>;
|
||||
getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
getInvalidThumbnail(): Promise<string[]>;
|
||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||
getShared(ownerId: string): Promise<AlbumEntity[]>;
|
||||
getNotShared(ownerId: string): Promise<AlbumEntity[]>;
|
||||
restoreAll(userId: string): Promise<void>;
|
||||
softDeleteAll(userId: string): Promise<void>;
|
||||
deleteAll(userId: string): Promise<void>;
|
||||
getAll(): Promise<AlbumEntity[]>;
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { AssetFileType, AssetOrder, AssetType } from 'src/enum';
|
||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||
@@ -56,6 +56,7 @@ export interface AssetBuilderOptions {
|
||||
userIds?: string[];
|
||||
withStacked?: boolean;
|
||||
exifInfo?: boolean;
|
||||
status?: AssetStatus;
|
||||
assetType?: AssetType;
|
||||
}
|
||||
|
||||
@@ -147,8 +148,6 @@ export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffli
|
||||
export const IAssetRepository = 'IAssetRepository';
|
||||
|
||||
export interface IAssetRepository {
|
||||
getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]>;
|
||||
getUniqueOriginalPaths(userId: string): Promise<string[]>;
|
||||
create(asset: AssetCreate): Promise<AssetEntity>;
|
||||
getByIds(
|
||||
ids: string[],
|
||||
@@ -176,7 +175,6 @@ export interface IAssetRepository {
|
||||
withDeleted?: boolean,
|
||||
): Paginated<AssetEntity>;
|
||||
getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
|
||||
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||
getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity>;
|
||||
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>;
|
||||
@@ -188,8 +186,6 @@ export interface IAssetRepository {
|
||||
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
|
||||
update(asset: AssetUpdateOptions): Promise<void>;
|
||||
remove(asset: AssetEntity): Promise<void>;
|
||||
softDeleteAll(ids: string[]): Promise<void>;
|
||||
restoreAll(ids: string[]): Promise<void>;
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
||||
|
||||
@@ -27,6 +27,7 @@ type EmitEventMap = {
|
||||
|
||||
// asset bulk events
|
||||
'assets.trash': [{ assetIds: string[]; userId: string }];
|
||||
'assets.delete': [{ assetIds: string[]; userId: string }];
|
||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||
|
||||
// session events
|
||||
|
||||
@@ -93,6 +93,8 @@ export enum JobName {
|
||||
QUEUE_SMART_SEARCH = 'queue-smart-search',
|
||||
SMART_SEARCH = 'smart-search',
|
||||
|
||||
QUEUE_TRASH_EMPTY = 'queue-trash-empty',
|
||||
|
||||
// duplicate detection
|
||||
QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection',
|
||||
DUPLICATE_DETECTION = 'duplicate-detection',
|
||||
@@ -253,6 +255,7 @@ export type JobItem =
|
||||
// Smart Search
|
||||
| { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob }
|
||||
| { name: JobName.SMART_SEARCH; data: IEntityJob }
|
||||
| { name: JobName.QUEUE_TRASH_EMPTY; data?: IBaseJob }
|
||||
|
||||
// Duplicate Detection
|
||||
| { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob }
|
||||
@@ -306,7 +309,6 @@ export interface IJobRepository {
|
||||
addHandler(queueName: QueueName, concurrency: number, handler: JobItemHandler): void;
|
||||
addCronJob(name: string, expression: string, onTick: () => void, start?: boolean): void;
|
||||
updateCronJob(name: string, expression?: string, start?: boolean): void;
|
||||
deleteCronJob(name: string): void;
|
||||
setConcurrency(queueName: QueueName, concurrency: number): void;
|
||||
queue(item: JobItem): Promise<void>;
|
||||
queueAll(items: JobItem[]): Promise<void>;
|
||||
|
||||
@@ -53,9 +53,4 @@ export interface IMetadataRepository {
|
||||
readTags(path: string): Promise<ImmichTags>;
|
||||
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
||||
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
||||
getCountries(userIds: string[]): Promise<Array<string | null>>;
|
||||
getStates(userIds: string[], country?: string): Promise<Array<string | null>>;
|
||||
getCities(userIds: string[], country?: string, state?: string): Promise<Array<string | null>>;
|
||||
getCameraMakes(userIds: string[], model?: string): Promise<Array<string | null>>;
|
||||
getCameraModels(userIds: string[], make?: string): Promise<Array<string | null>>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import { Paginated } from 'src/utils/pagination';
|
||||
|
||||
export const ISearchRepository = 'ISearchRepository';
|
||||
@@ -61,6 +61,7 @@ export interface SearchStatusOptions {
|
||||
isVisible?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
type?: AssetType;
|
||||
status?: AssetStatus;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
@@ -181,4 +182,9 @@ export interface ISearchRepository {
|
||||
deleteAllSearchEmbeddings(): Promise<void>;
|
||||
getDimensionSize(): Promise<number>;
|
||||
setDimensionSize(dimSize: number): Promise<void>;
|
||||
getCountries(userIds: string[]): Promise<Array<string | null>>;
|
||||
getStates(userIds: string[], country?: string): Promise<Array<string | null>>;
|
||||
getCities(userIds: string[], country?: string, state?: string): Promise<Array<string | null>>;
|
||||
getCameraMakes(userIds: string[], model?: string): Promise<Array<string | null>>;
|
||||
getCameraModels(userIds: string[], make?: string): Promise<Array<string | null>>;
|
||||
}
|
||||
|
||||
10
server/src/interfaces/trash.interface.ts
Normal file
10
server/src/interfaces/trash.interface.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
|
||||
export const ITrashRepository = 'ITrashRepository';
|
||||
|
||||
export interface ITrashRepository {
|
||||
empty(userId: string): Promise<number>;
|
||||
restore(userId: string): Promise<number>;
|
||||
restoreAll(assetIds: string[]): Promise<number>;
|
||||
getDeletedIds(pagination: PaginationOptions): Paginated<string>;
|
||||
}
|
||||
8
server/src/interfaces/view.interface.ts
Normal file
8
server/src/interfaces/view.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export const IViewRepository = 'IViewRepository';
|
||||
|
||||
export interface IViewRepository {
|
||||
getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]>;
|
||||
getUniqueOriginalPaths(userId: string): Promise<string[]>;
|
||||
}
|
||||
@@ -18,7 +18,9 @@ async function bootstrapImmichAdmin() {
|
||||
function bootstrapWorker(name: string) {
|
||||
console.log(`Starting ${name} worker`);
|
||||
|
||||
const worker = name === 'api' ? fork(`./dist/workers/${name}.js`) : new Worker(`./dist/workers/${name}.js`);
|
||||
const execArgv = process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg));
|
||||
const worker =
|
||||
name === 'api' ? fork(`./dist/workers/${name}.js`, [], { execArgv }) : new Worker(`./dist/workers/${name}.js`);
|
||||
|
||||
worker.on('error', (error) => {
|
||||
console.error(`${name} worker error: ${error}`);
|
||||
|
||||
14
server/src/migrations/1726491047923-AddprofileChangedAt.ts
Normal file
14
server/src/migrations/1726491047923-AddprofileChangedAt.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddprofileChangedAt1726491047923 implements MigrationInterface {
|
||||
name = 'AddprofileChangedAt1726491047923'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "profileChangedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profileChangedAt"`);
|
||||
}
|
||||
|
||||
}
|
||||
16
server/src/migrations/1726593009549-AddAssetStatus.ts
Normal file
16
server/src/migrations/1726593009549-AddAssetStatus.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddAssetStatus1726593009549 implements MigrationInterface {
|
||||
name = 'AddAssetStatus1726593009549'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TYPE "assets_status_enum" AS ENUM('active', 'trashed', 'deleted')`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD "status" "assets_status_enum" NOT NULL DEFAULT 'active'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "status"`);
|
||||
await queryRunner.query(`DROP TYPE "assets_status_enum"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,7 +23,8 @@ SELECT
|
||||
"ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status",
|
||||
"ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt",
|
||||
"ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes",
|
||||
"ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes"
|
||||
"ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes",
|
||||
"ActivityEntity__ActivityEntity_user"."profileChangedAt" AS "ActivityEntity__ActivityEntity_user_profileChangedAt"
|
||||
FROM
|
||||
"activity" "ActivityEntity"
|
||||
LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId"
|
||||
|
||||
@@ -30,6 +30,7 @@ FROM
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
@@ -47,6 +48,7 @@ FROM
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
@@ -80,64 +82,6 @@ ORDER BY
|
||||
LIMIT
|
||||
1
|
||||
|
||||
-- AlbumRepository.getByIds
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
((("AlbumEntity"."id" IN ($1))))
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- AlbumRepository.getByAssetId
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
@@ -164,6 +108,7 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
@@ -180,7 +125,8 @@ SELECT
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
@@ -241,35 +187,6 @@ WHERE
|
||||
GROUP BY
|
||||
"album"."id"
|
||||
|
||||
-- AlbumRepository.getInvalidThumbnail
|
||||
SELECT
|
||||
"albums"."id" AS "albums_id"
|
||||
FROM
|
||||
"albums" "albums"
|
||||
WHERE
|
||||
(
|
||||
"albums"."albumThumbnailAssetId" IS NULL
|
||||
AND EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
"albums_assets_assets" "albums_assets"
|
||||
WHERE
|
||||
"albums"."id" = "albums_assets"."albumsId"
|
||||
)
|
||||
OR "albums"."albumThumbnailAssetId" IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
"albums_assets_assets" "albums_assets"
|
||||
WHERE
|
||||
"albums"."id" = "albums_assets"."albumsId"
|
||||
AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"
|
||||
)
|
||||
)
|
||||
AND ("albums"."deletedAt" IS NULL)
|
||||
|
||||
-- AlbumRepository.getOwned
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
@@ -299,6 +216,7 @@ SELECT
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
@@ -324,7 +242,8 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
@@ -372,6 +291,7 @@ SELECT
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
@@ -397,7 +317,8 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
@@ -495,7 +416,8 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
@@ -528,41 +450,6 @@ WHERE
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
|
||||
-- AlbumRepository.getAll
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
"AlbumEntity"."deletedAt" IS NULL
|
||||
|
||||
-- AlbumRepository.removeAsset
|
||||
DELETE FROM "albums_assets_assets"
|
||||
WHERE
|
||||
|
||||
@@ -24,6 +24,7 @@ FROM
|
||||
"APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
|
||||
"APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",
|
||||
"APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes",
|
||||
"APIKeyEntity__APIKeyEntity_user"."profileChangedAt" AS "APIKeyEntity__APIKeyEntity_user_profileChangedAt",
|
||||
"7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_userId",
|
||||
"7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."key" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_key",
|
||||
"7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."value" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_value"
|
||||
|
||||
@@ -8,6 +8,7 @@ SELECT
|
||||
"entity"."libraryId" AS "entity_libraryId",
|
||||
"entity"."deviceId" AS "entity_deviceId",
|
||||
"entity"."type" AS "entity_type",
|
||||
"entity"."status" AS "entity_status",
|
||||
"entity"."originalPath" AS "entity_originalPath",
|
||||
"entity"."thumbhash" AS "entity_thumbhash",
|
||||
"entity"."encodedVideoPath" AS "entity_encodedVideoPath",
|
||||
@@ -96,6 +97,7 @@ SELECT
|
||||
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
|
||||
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
|
||||
"AssetEntity"."type" AS "AssetEntity_type",
|
||||
"AssetEntity"."status" AS "AssetEntity_status",
|
||||
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
|
||||
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
|
||||
@@ -130,6 +132,7 @@ SELECT
|
||||
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
|
||||
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
|
||||
"AssetEntity"."type" AS "AssetEntity_type",
|
||||
"AssetEntity"."status" AS "AssetEntity_status",
|
||||
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
|
||||
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
|
||||
@@ -218,6 +221,7 @@ SELECT
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."libraryId" AS "bd93d5747511a4dad4923546c51365bf1a803774_libraryId",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."status" AS "bd93d5747511a4dad4923546c51365bf1a803774_status",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath",
|
||||
@@ -305,6 +309,7 @@ FROM
|
||||
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
|
||||
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
|
||||
"AssetEntity"."type" AS "AssetEntity_type",
|
||||
"AssetEntity"."status" AS "AssetEntity_status",
|
||||
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
|
||||
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
|
||||
@@ -402,6 +407,7 @@ SELECT
|
||||
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
|
||||
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
|
||||
"AssetEntity"."type" AS "AssetEntity_type",
|
||||
"AssetEntity"."status" AS "AssetEntity_status",
|
||||
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
|
||||
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
|
||||
@@ -455,6 +461,7 @@ SELECT
|
||||
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
|
||||
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
|
||||
"AssetEntity"."type" AS "AssetEntity_type",
|
||||
"AssetEntity"."status" AS "AssetEntity_status",
|
||||
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
|
||||
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
|
||||
@@ -527,6 +534,7 @@ SELECT
|
||||
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
|
||||
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
|
||||
"AssetEntity"."type" AS "AssetEntity_type",
|
||||
"AssetEntity"."status" AS "AssetEntity_status",
|
||||
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
|
||||
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
|
||||
@@ -581,6 +589,7 @@ SELECT
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -640,6 +649,7 @@ SELECT
|
||||
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
|
||||
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
|
||||
"stackedAssets"."type" AS "stackedAssets_type",
|
||||
"stackedAssets"."status" AS "stackedAssets_status",
|
||||
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
|
||||
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
|
||||
@@ -719,6 +729,7 @@ SELECT
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -778,6 +789,7 @@ SELECT
|
||||
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
|
||||
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
|
||||
"stackedAssets"."type" AS "stackedAssets_type",
|
||||
"stackedAssets"."status" AS "stackedAssets_status",
|
||||
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
|
||||
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
|
||||
@@ -833,6 +845,7 @@ SELECT
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -892,6 +905,7 @@ SELECT
|
||||
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
|
||||
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
|
||||
"stackedAssets"."type" AS "stackedAssets_type",
|
||||
"stackedAssets"."status" AS "stackedAssets_status",
|
||||
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
|
||||
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
|
||||
@@ -997,6 +1011,7 @@ SELECT
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -1072,6 +1087,7 @@ SELECT
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -1134,109 +1150,6 @@ WHERE
|
||||
AND "asset"."ownerId" IN ($1)
|
||||
AND "asset"."updatedAt" > $2
|
||||
|
||||
-- AssetRepository.getAssetsByOriginalPath
|
||||
SELECT
|
||||
"asset"."id" AS "asset_id",
|
||||
"asset"."deviceAssetId" AS "asset_deviceAssetId",
|
||||
"asset"."ownerId" AS "asset_ownerId",
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
"asset"."createdAt" AS "asset_createdAt",
|
||||
"asset"."updatedAt" AS "asset_updatedAt",
|
||||
"asset"."deletedAt" AS "asset_deletedAt",
|
||||
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
|
||||
"asset"."localDateTime" AS "asset_localDateTime",
|
||||
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
|
||||
"asset"."isFavorite" AS "asset_isFavorite",
|
||||
"asset"."isArchived" AS "asset_isArchived",
|
||||
"asset"."isExternal" AS "asset_isExternal",
|
||||
"asset"."isOffline" AS "asset_isOffline",
|
||||
"asset"."checksum" AS "asset_checksum",
|
||||
"asset"."duration" AS "asset_duration",
|
||||
"asset"."isVisible" AS "asset_isVisible",
|
||||
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
"exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
|
||||
"exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
|
||||
"exifInfo"."orientation" AS "exifInfo_orientation",
|
||||
"exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
|
||||
"exifInfo"."modifyDate" AS "exifInfo_modifyDate",
|
||||
"exifInfo"."timeZone" AS "exifInfo_timeZone",
|
||||
"exifInfo"."latitude" AS "exifInfo_latitude",
|
||||
"exifInfo"."longitude" AS "exifInfo_longitude",
|
||||
"exifInfo"."projectionType" AS "exifInfo_projectionType",
|
||||
"exifInfo"."city" AS "exifInfo_city",
|
||||
"exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
|
||||
"exifInfo"."autoStackId" AS "exifInfo_autoStackId",
|
||||
"exifInfo"."state" AS "exifInfo_state",
|
||||
"exifInfo"."country" AS "exifInfo_country",
|
||||
"exifInfo"."make" AS "exifInfo_make",
|
||||
"exifInfo"."model" AS "exifInfo_model",
|
||||
"exifInfo"."lensModel" AS "exifInfo_lensModel",
|
||||
"exifInfo"."fNumber" AS "exifInfo_fNumber",
|
||||
"exifInfo"."focalLength" AS "exifInfo_focalLength",
|
||||
"exifInfo"."iso" AS "exifInfo_iso",
|
||||
"exifInfo"."exposureTime" AS "exifInfo_exposureTime",
|
||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."rating" AS "exifInfo_rating",
|
||||
"exifInfo"."fps" AS "exifInfo_fps",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId",
|
||||
"stackedAssets"."id" AS "stackedAssets_id",
|
||||
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
|
||||
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
|
||||
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
|
||||
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
|
||||
"stackedAssets"."type" AS "stackedAssets_type",
|
||||
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
|
||||
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
|
||||
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
|
||||
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
|
||||
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
|
||||
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
|
||||
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
|
||||
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
|
||||
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
|
||||
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
|
||||
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
|
||||
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
|
||||
"stackedAssets"."checksum" AS "stackedAssets_checksum",
|
||||
"stackedAssets"."duration" AS "stackedAssets_duration",
|
||||
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
|
||||
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
|
||||
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
|
||||
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId",
|
||||
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
|
||||
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
|
||||
AND ("stackedAssets"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" = $1
|
||||
AND (
|
||||
"asset"."originalPath" LIKE $2
|
||||
AND "asset"."originalPath" NOT LIKE $3
|
||||
)
|
||||
ORDER BY
|
||||
regexp_replace("asset"."originalPath", '.*/(.+)', '\1') ASC
|
||||
|
||||
-- AssetRepository.upsertFile
|
||||
INSERT INTO
|
||||
"asset_files" (
|
||||
|
||||
@@ -28,7 +28,8 @@ FROM
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes",
|
||||
"LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
|
||||
@@ -68,7 +69,8 @@ SELECT
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes",
|
||||
"LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
|
||||
@@ -104,7 +106,8 @@ SELECT
|
||||
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
|
||||
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
|
||||
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes",
|
||||
"LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- MetadataRepository.getCountries
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."country") "exif"."country" AS "country"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
|
||||
-- MetadataRepository.getStates
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."state") "exif"."state" AS "state"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."country" = $2
|
||||
|
||||
-- MetadataRepository.getCities
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."city") "exif"."city" AS "city"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."country" = $2
|
||||
AND "exif"."state" = $3
|
||||
|
||||
-- MetadataRepository.getCameraMakes
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."make") "exif"."make" AS "make"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."model" = $2
|
||||
|
||||
-- MetadataRepository.getCameraModels
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."model") "exif"."model" AS "model"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."make" = $2
|
||||
@@ -159,6 +159,7 @@ FROM
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
|
||||
@@ -260,6 +261,7 @@ FROM
|
||||
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
|
||||
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
|
||||
"AssetEntity"."type" AS "AssetEntity_type",
|
||||
"AssetEntity"."status" AS "AssetEntity_status",
|
||||
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
|
||||
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
|
||||
@@ -391,6 +393,7 @@ SELECT
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
|
||||
|
||||
@@ -13,6 +13,7 @@ FROM
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -43,6 +44,7 @@ FROM
|
||||
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
|
||||
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
|
||||
"stackedAssets"."type" AS "stackedAssets_type",
|
||||
"stackedAssets"."status" AS "stackedAssets_status",
|
||||
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
|
||||
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
|
||||
@@ -106,6 +108,7 @@ SELECT
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -136,6 +139,7 @@ SELECT
|
||||
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
|
||||
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
|
||||
"stackedAssets"."type" AS "stackedAssets_type",
|
||||
"stackedAssets"."status" AS "stackedAssets_status",
|
||||
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
|
||||
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
|
||||
@@ -345,6 +349,7 @@ SELECT
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
@@ -401,3 +406,58 @@ FROM
|
||||
INNER JOIN cte ON asset.id = cte."assetId"
|
||||
ORDER BY
|
||||
exif.city
|
||||
|
||||
-- SearchRepository.getCountries
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."country") "exif"."country" AS "country"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
|
||||
-- SearchRepository.getStates
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."state") "exif"."state" AS "state"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."country" = $2
|
||||
|
||||
-- SearchRepository.getCities
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."city") "exif"."city" AS "city"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."country" = $2
|
||||
AND "exif"."state" = $3
|
||||
|
||||
-- SearchRepository.getCameraMakes
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."make") "exif"."make" AS "make"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."model" = $2
|
||||
|
||||
-- SearchRepository.getCameraModels
|
||||
SELECT DISTINCT
|
||||
ON ("exif"."model") "exif"."model" AS "model"
|
||||
FROM
|
||||
"exif" "exif"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"asset"."ownerId" IN ($1)
|
||||
AND "exif"."make" = $2
|
||||
|
||||
@@ -39,6 +39,7 @@ FROM
|
||||
"SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt",
|
||||
"SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes",
|
||||
"SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes",
|
||||
"SessionEntity__SessionEntity_user"."profileChangedAt" AS "SessionEntity__SessionEntity_user_profileChangedAt",
|
||||
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId",
|
||||
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key",
|
||||
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value"
|
||||
|
||||
@@ -27,6 +27,7 @@ FROM
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
|
||||
@@ -93,6 +94,7 @@ FROM
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."libraryId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_libraryId",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."status" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_status",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath",
|
||||
@@ -156,7 +158,8 @@ FROM
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt"
|
||||
FROM
|
||||
"shared_links" "SharedLinkEntity"
|
||||
LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
|
||||
@@ -213,6 +216,7 @@ SELECT
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
|
||||
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
|
||||
@@ -257,7 +261,8 @@ SELECT
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt"
|
||||
FROM
|
||||
"shared_links" "SharedLinkEntity"
|
||||
LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
|
||||
@@ -309,7 +314,8 @@ FROM
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes"
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes",
|
||||
"SharedLinkEntity__SharedLinkEntity_user"."profileChangedAt" AS "SharedLinkEntity__SharedLinkEntity_user_profileChangedAt"
|
||||
FROM
|
||||
"shared_links" "SharedLinkEntity"
|
||||
LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId"
|
||||
|
||||
@@ -15,7 +15,8 @@ SELECT
|
||||
"UserEntity"."status" AS "UserEntity_status",
|
||||
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
|
||||
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
|
||||
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
|
||||
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes",
|
||||
"UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt"
|
||||
FROM
|
||||
"users" "UserEntity"
|
||||
WHERE
|
||||
@@ -60,7 +61,8 @@ SELECT
|
||||
"user"."status" AS "user_status",
|
||||
"user"."updatedAt" AS "user_updatedAt",
|
||||
"user"."quotaSizeInBytes" AS "user_quotaSizeInBytes",
|
||||
"user"."quotaUsageInBytes" AS "user_quotaUsageInBytes"
|
||||
"user"."quotaUsageInBytes" AS "user_quotaUsageInBytes",
|
||||
"user"."profileChangedAt" AS "user_profileChangedAt"
|
||||
FROM
|
||||
"users" "user"
|
||||
WHERE
|
||||
@@ -82,7 +84,8 @@ SELECT
|
||||
"UserEntity"."status" AS "UserEntity_status",
|
||||
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
|
||||
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
|
||||
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
|
||||
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes",
|
||||
"UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt"
|
||||
FROM
|
||||
"users" "UserEntity"
|
||||
WHERE
|
||||
@@ -106,7 +109,8 @@ SELECT
|
||||
"UserEntity"."status" AS "UserEntity_status",
|
||||
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
|
||||
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
|
||||
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
|
||||
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes",
|
||||
"UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt"
|
||||
FROM
|
||||
"users" "UserEntity"
|
||||
WHERE
|
||||
|
||||
79
server/src/queries/view.repository.sql
Normal file
79
server/src/queries/view.repository.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- ViewRepository.getAssetsByOriginalPath
|
||||
SELECT
|
||||
"asset"."id" AS "asset_id",
|
||||
"asset"."deviceAssetId" AS "asset_deviceAssetId",
|
||||
"asset"."ownerId" AS "asset_ownerId",
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."status" AS "asset_status",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
"asset"."createdAt" AS "asset_createdAt",
|
||||
"asset"."updatedAt" AS "asset_updatedAt",
|
||||
"asset"."deletedAt" AS "asset_deletedAt",
|
||||
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
|
||||
"asset"."localDateTime" AS "asset_localDateTime",
|
||||
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
|
||||
"asset"."isFavorite" AS "asset_isFavorite",
|
||||
"asset"."isArchived" AS "asset_isArchived",
|
||||
"asset"."isExternal" AS "asset_isExternal",
|
||||
"asset"."isOffline" AS "asset_isOffline",
|
||||
"asset"."checksum" AS "asset_checksum",
|
||||
"asset"."duration" AS "asset_duration",
|
||||
"asset"."isVisible" AS "asset_isVisible",
|
||||
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
"exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
|
||||
"exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
|
||||
"exifInfo"."orientation" AS "exifInfo_orientation",
|
||||
"exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
|
||||
"exifInfo"."modifyDate" AS "exifInfo_modifyDate",
|
||||
"exifInfo"."timeZone" AS "exifInfo_timeZone",
|
||||
"exifInfo"."latitude" AS "exifInfo_latitude",
|
||||
"exifInfo"."longitude" AS "exifInfo_longitude",
|
||||
"exifInfo"."projectionType" AS "exifInfo_projectionType",
|
||||
"exifInfo"."city" AS "exifInfo_city",
|
||||
"exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
|
||||
"exifInfo"."autoStackId" AS "exifInfo_autoStackId",
|
||||
"exifInfo"."state" AS "exifInfo_state",
|
||||
"exifInfo"."country" AS "exifInfo_country",
|
||||
"exifInfo"."make" AS "exifInfo_make",
|
||||
"exifInfo"."model" AS "exifInfo_model",
|
||||
"exifInfo"."lensModel" AS "exifInfo_lensModel",
|
||||
"exifInfo"."fNumber" AS "exifInfo_fNumber",
|
||||
"exifInfo"."focalLength" AS "exifInfo_focalLength",
|
||||
"exifInfo"."iso" AS "exifInfo_iso",
|
||||
"exifInfo"."exposureTime" AS "exifInfo_exposureTime",
|
||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."rating" AS "exifInfo_rating",
|
||||
"exifInfo"."fps" AS "exifInfo_fps"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||
WHERE
|
||||
(
|
||||
(
|
||||
"asset"."isVisible" = $1
|
||||
AND "asset"."isArchived" = $2
|
||||
AND "asset"."ownerId" = $3
|
||||
)
|
||||
AND (
|
||||
"asset"."originalPath" LIKE $4
|
||||
AND "asset"."originalPath" NOT LIKE $5
|
||||
)
|
||||
)
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
regexp_replace("asset"."originalPath", '.*/(.+)', '\1') ASC
|
||||
@@ -57,22 +57,6 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return withoutDeletedUsers(album);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@ChunkedArray()
|
||||
async getByIds(ids: string[]): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
where: {
|
||||
id: In(ids),
|
||||
},
|
||||
relations: {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
},
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
@@ -116,34 +100,6 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the album IDs that have an invalid thumbnail, when:
|
||||
* - Thumbnail references an asset outside the album
|
||||
* - Empty album still has a thumbnail set
|
||||
*/
|
||||
@GenerateSql()
|
||||
async getInvalidThumbnail(): Promise<string[]> {
|
||||
// Using dataSource, because there is no direct access to albums_assets_assets.
|
||||
const albumHasAssets = this.dataSource
|
||||
.createQueryBuilder()
|
||||
.select('1')
|
||||
.from('albums_assets_assets', 'albums_assets')
|
||||
.where('"albums"."id" = "albums_assets"."albumsId"');
|
||||
|
||||
const albumContainsThumbnail = albumHasAssets
|
||||
.clone()
|
||||
.andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"');
|
||||
|
||||
const albums = await this.repository
|
||||
.createQueryBuilder('albums')
|
||||
.select('albums.id')
|
||||
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`)
|
||||
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`)
|
||||
.getMany();
|
||||
|
||||
return albums.map((album) => album.id);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getOwned(ownerId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
@@ -199,15 +155,6 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
await this.repository.delete({ ownerId: userId });
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAll(): Promise<AlbumEntity[]> {
|
||||
return this.repository.find({
|
||||
relations: {
|
||||
owner: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async removeAsset(assetId: string): Promise<void> {
|
||||
// Using dataSource, because there is no direct access to albums_assets_assets.
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import { AssetFileType, AssetOrder, AssetType } from 'src/enum';
|
||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||
import {
|
||||
AssetBuilderOptions,
|
||||
AssetCreate,
|
||||
@@ -295,16 +295,6 @@ export class AssetRepository implements IAssetRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Chunked()
|
||||
async softDeleteAll(ids: string[]): Promise<void> {
|
||||
await this.repository.softDelete({ id: In(ids) });
|
||||
}
|
||||
|
||||
@Chunked()
|
||||
async restoreAll(ids: string[]): Promise<void> {
|
||||
await this.repository.restore({ id: In(ids) });
|
||||
}
|
||||
|
||||
async update(asset: AssetUpdateOptions): Promise<void> {
|
||||
await this.repository.update(asset.id, asset);
|
||||
}
|
||||
@@ -571,13 +561,6 @@ export class AssetRepository implements IAssetRepository {
|
||||
});
|
||||
}
|
||||
|
||||
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: { albums: { id: albumId } },
|
||||
order: { fileCreatedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: { albums: { id: albumId } },
|
||||
@@ -604,7 +587,10 @@ export class AssetRepository implements IAssetRepository {
|
||||
}
|
||||
|
||||
if (isTrashed !== undefined) {
|
||||
builder.withDeleted().andWhere(`asset.deletedAt is not null`);
|
||||
builder
|
||||
.withDeleted()
|
||||
.andWhere(`asset.deletedAt is not null`)
|
||||
.andWhere('asset.status = :status', { status: AssetStatus.TRASHED });
|
||||
}
|
||||
|
||||
const items = await builder.getRawMany();
|
||||
@@ -762,6 +748,10 @@ export class AssetRepository implements IAssetRepository {
|
||||
|
||||
if (options.isTrashed !== undefined) {
|
||||
builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted();
|
||||
|
||||
if (options.isTrashed) {
|
||||
builder.andWhere('asset.status = :status', { status: AssetStatus.TRASHED });
|
||||
}
|
||||
}
|
||||
|
||||
if (options.isDuplicate !== undefined) {
|
||||
@@ -836,50 +826,6 @@ export class AssetRepository implements IAssetRepository {
|
||||
return builder.getMany();
|
||||
}
|
||||
|
||||
async getUniqueOriginalPaths(userId: string): Promise<string[]> {
|
||||
const builder = this.getBuilder({
|
||||
userIds: [userId],
|
||||
exifInfo: false,
|
||||
withStacked: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
});
|
||||
|
||||
const results = await builder
|
||||
.select("DISTINCT substring(asset.originalPath FROM '^(.*/)[^/]*$')", 'directoryPath')
|
||||
.getRawMany();
|
||||
|
||||
return results.map((row: { directoryPath: string }) => row.directoryPath.replaceAll(/^\/|\/$/g, ''));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
async getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]> {
|
||||
const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, '');
|
||||
|
||||
const builder = this.getBuilder({
|
||||
userIds: [userId],
|
||||
exifInfo: true,
|
||||
withStacked: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
});
|
||||
|
||||
const assets = await builder
|
||||
.where('asset.ownerId = :userId', { userId })
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('asset.originalPath LIKE :likePath', { likePath: `%${normalizedPath}/%` }).andWhere(
|
||||
'asset.originalPath NOT LIKE :notLikePath',
|
||||
{ notLikePath: `%${normalizedPath}/%/%` },
|
||||
);
|
||||
}),
|
||||
)
|
||||
.orderBy(String.raw`regexp_replace(asset.originalPath, '.*/(.+)', '\1')`, 'ASC')
|
||||
.getMany();
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] })
|
||||
async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise<void> {
|
||||
await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user