Merge branch 'main' into feat/search-filter-album/web

This commit is contained in:
CJPeckover
2025-07-15 14:18:43 -04:00
106 changed files with 4206 additions and 1756 deletions
+2 -2
View File
@@ -131,7 +131,7 @@ jobs:
tag-suffix: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
permissions:
contents: read
actions: read
@@ -154,7 +154,7 @@ jobs:
name: Build and Push Server
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
permissions:
contents: read
actions: read
+59 -59
View File
@@ -1365,17 +1365,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
"integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/type-utils": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/type-utils": "8.36.0",
"@typescript-eslint/utils": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -1389,7 +1389,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.35.1",
"@typescript-eslint/parser": "^8.36.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@@ -1405,16 +1405,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/typescript-estree": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1430,14 +1430,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
"integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.1",
"@typescript-eslint/types": "^8.35.1",
"@typescript-eslint/tsconfig-utils": "^8.36.0",
"@typescript-eslint/types": "^8.36.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1452,14 +1452,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
"integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1"
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1470,9 +1470,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
"integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1487,14 +1487,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
"integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/typescript-estree": "8.36.0",
"@typescript-eslint/utils": "8.36.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -1511,9 +1511,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
"integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1525,16 +1525,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
"integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.35.1",
"@typescript-eslint/tsconfig-utils": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"@typescript-eslint/project-service": "8.36.0",
"@typescript-eslint/tsconfig-utils": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1580,16 +1580,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
"integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1"
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/typescript-estree": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1604,13 +1604,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
"integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/types": "8.36.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -4125,15 +4125,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
"integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.35.1",
"@typescript-eslint/utils": "8.35.1"
"@typescript-eslint/eslint-plugin": "8.36.0",
"@typescript-eslint/parser": "8.36.0",
"@typescript-eslint/utils": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+1 -1
View File
@@ -116,7 +116,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
healthcheck:
test: redis-cli ping || exit 1
+2 -2
View File
@@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -83,7 +83,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:7a34573f0b9c952286b33d537f233cd5b708e12263733aa646e50c33f598f16c
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
+1 -1
View File
@@ -49,7 +49,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
healthcheck:
test: redis-cli ping || exit 1
restart: always
+70 -70
View File
@@ -83,7 +83,7 @@
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"typescript-eslint": "^8.28.0",
"vite": "^6.0.0",
"vite": "^7.0.0",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0",
@@ -2101,17 +2101,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
"integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/type-utils": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/type-utils": "8.36.0",
"@typescript-eslint/utils": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -2125,7 +2125,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.35.1",
"@typescript-eslint/parser": "^8.36.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@@ -2141,16 +2141,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/typescript-estree": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2166,14 +2166,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
"integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.1",
"@typescript-eslint/types": "^8.35.1",
"@typescript-eslint/tsconfig-utils": "^8.36.0",
"@typescript-eslint/types": "^8.36.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2188,14 +2188,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
"integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1"
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2206,9 +2206,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
"integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2223,14 +2223,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
"integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/typescript-estree": "8.36.0",
"@typescript-eslint/utils": "8.36.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -2247,9 +2247,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
"integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2261,16 +2261,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
"integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.35.1",
"@typescript-eslint/tsconfig-utils": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.35.1",
"@typescript-eslint/project-service": "8.36.0",
"@typescript-eslint/tsconfig-utils": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -2316,16 +2316,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
"integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.35.1"
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/typescript-estree": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2340,13 +2340,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
"integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.1",
"@typescript-eslint/types": "8.36.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -4812,9 +4812,9 @@
"license": "ISC"
},
"node_modules/luxon": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
"integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6430,9 +6430,9 @@
}
},
"node_modules/superagent": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz",
"integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==",
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz",
"integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6451,14 +6451,14 @@
}
},
"node_modules/supertest": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz",
"integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz",
"integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"methods": "^1.1.2",
"superagent": "^10.2.1"
"superagent": "^10.2.2"
},
"engines": {
"node": ">=14.18.0"
@@ -6778,15 +6778,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
"integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.35.1",
"@typescript-eslint/utils": "8.35.1"
"@typescript-eslint/eslint-plugin": "8.36.0",
"@typescript-eslint/parser": "8.36.0",
"@typescript-eslint/utils": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+4 -1
View File
@@ -4,9 +4,12 @@ import sys
import requests
port = os.getenv("IMMICH_PORT", 3003)
host = os.getenv("IMMICH_HOST", "0.0.0.0")
host = "localhost" if host == "0.0.0.0" else host
try:
response = requests.get(f"http://localhost:{port}/ping", timeout=2)
response = requests.get(f"http://{host}:{port}/ping", timeout=2)
if response.status_code == 200:
sys.exit(0)
sys.exit(1)
File diff suppressed because one or more lines are too long
+3 -9
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -117,6 +117,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
@@ -471,14 +473,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -507,14 +505,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@@ -43,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
@@ -43,6 +43,8 @@ sealed class BaseAsset {
bool get isImage => type == AssetType.image;
bool get isVideo => type == AssetType.video;
bool get isMotionPhoto => livePhotoVideoId != null;
Duration get duration {
final durationInSeconds = this.durationInSeconds;
if (durationInSeconds != null) {
@@ -94,6 +94,7 @@ class RemoteAsset extends BaseAsset {
bool? isFavorite,
String? thumbHash,
AssetVisibility? visibility,
String? livePhotoVideoId,
}) {
return RemoteAsset(
id: id ?? this.id,
@@ -110,6 +111,7 @@ class RemoteAsset extends BaseAsset {
isFavorite: isFavorite ?? this.isFavorite,
thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
);
}
}
@@ -1,5 +1,12 @@
import 'dart:ui';
enum UserMetadataKey {
// do not change this order!
onboarding,
preferences,
license,
}
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary("primary"),
@@ -31,7 +38,45 @@ enum AvatarColor {
};
}
class UserPreferences {
class Onboarding {
final bool isOnboarded;
const Onboarding({required this.isOnboarded});
Onboarding copyWith({bool? isOnboarded}) {
return Onboarding(isOnboarded: isOnboarded ?? this.isOnboarded);
}
Map<String, Object?> toMap() {
final onboarding = <String, Object?>{};
onboarding["isOnboarded"] = isOnboarded;
return onboarding;
}
factory Onboarding.fromMap(Map<String, Object?> map) {
return Onboarding(isOnboarded: map["isOnboarded"] as bool);
}
@override
String toString() {
return '''Onboarding {
isOnboarded: $isOnboarded,
}''';
}
@override
bool operator ==(covariant Onboarding other) {
if (identical(this, other)) return true;
return isOnboarded == other.isOnboarded;
}
@override
int get hashCode => isOnboarded.hashCode;
}
// TODO: wait to be overwritten
class Preferences {
final bool foldersEnabled;
final bool memoriesEnabled;
final bool peopleEnabled;
@@ -41,7 +86,7 @@ class UserPreferences {
final AvatarColor userAvatarColor;
final bool showSupportBadge;
const UserPreferences({
const Preferences({
this.foldersEnabled = false,
this.memoriesEnabled = true,
this.peopleEnabled = true,
@@ -52,7 +97,7 @@ class UserPreferences {
this.showSupportBadge = true,
});
UserPreferences copyWith({
Preferences copyWith({
bool? foldersEnabled,
bool? memoriesEnabled,
bool? peopleEnabled,
@@ -62,7 +107,7 @@ class UserPreferences {
AvatarColor? userAvatarColor,
bool? showSupportBadge,
}) {
return UserPreferences(
return Preferences(
foldersEnabled: foldersEnabled ?? this.foldersEnabled,
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
peopleEnabled: peopleEnabled ?? this.peopleEnabled,
@@ -87,8 +132,8 @@ class UserPreferences {
return preferences;
}
factory UserPreferences.fromMap(Map<String, Object?> map) {
return UserPreferences(
factory Preferences.fromMap(Map<String, Object?> map) {
return Preferences(
foldersEnabled: map["folders-Enabled"] as bool? ?? false,
memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
peopleEnabled: map["people-Enabled"] as bool? ?? true,
@@ -102,4 +147,173 @@ class UserPreferences {
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
);
}
@override
String toString() {
return '''Preferences: {
foldersEnabled: $foldersEnabled,
memoriesEnabled: $memoriesEnabled,
peopleEnabled: $peopleEnabled,
ratingsEnabled: $ratingsEnabled,
sharedLinksEnabled: $sharedLinksEnabled,
tagsEnabled: $tagsEnabled,
userAvatarColor: $userAvatarColor,
showSupportBadge: $showSupportBadge,
}''';
}
@override
bool operator ==(covariant Preferences other) {
if (identical(this, other)) return true;
return other.foldersEnabled == foldersEnabled &&
other.memoriesEnabled == memoriesEnabled &&
other.peopleEnabled == peopleEnabled &&
other.ratingsEnabled == ratingsEnabled &&
other.sharedLinksEnabled == sharedLinksEnabled &&
other.tagsEnabled == tagsEnabled &&
other.userAvatarColor == userAvatarColor &&
other.showSupportBadge == showSupportBadge;
}
@override
int get hashCode {
return foldersEnabled.hashCode ^
memoriesEnabled.hashCode ^
peopleEnabled.hashCode ^
ratingsEnabled.hashCode ^
sharedLinksEnabled.hashCode ^
tagsEnabled.hashCode ^
userAvatarColor.hashCode ^
showSupportBadge.hashCode;
}
}
class License {
final DateTime activatedAt;
final String activationKey;
final String licenseKey;
const License({
required this.activatedAt,
required this.activationKey,
required this.licenseKey,
});
License copyWith({
DateTime? activatedAt,
String? activationKey,
String? licenseKey,
}) {
return License(
activatedAt: activatedAt ?? this.activatedAt,
activationKey: activationKey ?? this.activationKey,
licenseKey: licenseKey ?? this.licenseKey,
);
}
Map<String, Object?> toMap() {
final license = <String, Object?>{};
license["activatedAt"] = activatedAt;
license["activationKey"] = activationKey;
license["licenseKey"] = licenseKey;
return license;
}
factory License.fromMap(Map<String, Object?> map) {
return License(
activatedAt: map["activatedAt"] as DateTime,
activationKey: map["activationKey"] as String,
licenseKey: map["licenseKey"] as String,
);
}
@override
String toString() {
return '''License {
activatedAt: $activatedAt,
activationKey: $activationKey,
licenseKey: $licenseKey,
}''';
}
@override
bool operator ==(covariant License other) {
if (identical(this, other)) return true;
return activatedAt == other.activatedAt &&
activationKey == other.activationKey &&
licenseKey == other.licenseKey;
}
@override
int get hashCode =>
activatedAt.hashCode ^ activationKey.hashCode ^ licenseKey.hashCode;
}
// Model for a user metadata stored in the server
class UserMetadata {
final String userId;
final UserMetadataKey key;
final Onboarding? onboarding;
final Preferences? preferences;
final License? license;
const UserMetadata({
required this.userId,
required this.key,
this.onboarding,
this.preferences,
this.license,
}) : assert(
onboarding != null || preferences != null || license != null,
'One of onboarding, preferences and license must be provided',
);
UserMetadata copyWith({
String? userId,
UserMetadataKey? key,
Onboarding? onboarding,
Preferences? preferences,
License? license,
}) {
return UserMetadata(
userId: userId ?? this.userId,
key: key ?? this.key,
onboarding: onboarding ?? this.onboarding,
preferences: preferences ?? this.preferences,
license: license ?? this.license,
);
}
@override
String toString() {
return '''UserMetadata: {
userId: $userId,
key: $key,
onboarding: ${onboarding ?? "<NA>"},
preferences: ${preferences ?? "<NA>"},
license: ${license ?? "<NA>"},
}''';
}
@override
bool operator ==(covariant UserMetadata other) {
if (identical(this, other)) return true;
return other.userId == userId &&
other.key == key &&
other.onboarding == onboarding &&
other.preferences == preferences &&
other.license == license;
}
@override
int get hashCode {
return userId.hashCode ^
key.hashCode ^
onboarding.hashCode ^
preferences.hashCode ^
license.hashCode;
}
}
@@ -61,4 +61,8 @@ class AssetService {
return 1.0;
}
Future<List<(String, String)>> getPlaces() {
return _remoteAssetRepository.getPlaces();
}
}
@@ -31,6 +31,59 @@ class SyncStreamService {
return _syncApiRepository.streamChanges(_handleEvents);
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
_logger.info(
'Processing batch of ${batchData.length} AssetUploadReadyV1 events',
);
final List<SyncAssetV1> assets = [];
final List<SyncAssetExifV1> exifs = [];
try {
for (final data in batchData) {
if (data is! Map<String, dynamic>) {
continue;
}
final payload = data;
final assetData = payload['asset'];
final exifData = payload['exif'];
if (assetData == null || exifData == null) {
continue;
}
final asset = SyncAssetV1.fromJson(assetData);
final exif = SyncAssetExifV1.fromJson(exifData);
if (asset != null && exif != null) {
assets.add(asset);
exifs.add(exif);
}
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV1(
assets,
debugLabel: 'websocket-batch',
);
await _syncStreamRepository.updateAssetsExifV1(
exifs,
debugLabel: 'websocket-batch',
);
_logger.info('Successfully processed ${assets.length} assets in batch');
}
} catch (error, stackTrace) {
_logger.severe(
"Error processing AssetUploadReadyV1 websocket batch events",
error,
stackTrace,
);
}
}
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
List<SyncEvent> items = [];
for (final event in events) {
@@ -179,6 +232,14 @@ class SyncStreamService {
data.cast(),
debugLabel: 'partner',
);
case SyncEntityType.userMetadataV1:
return _syncStreamRepository.updateUserMetadatasV1(
data.cast(),
);
case SyncEntityType.userMetadataDeleteV1:
return _syncStreamRepository.deleteUserMetadatasV1(
data.cast(),
);
default:
_logger.warning("Unknown sync data type: $type");
}
@@ -62,6 +62,9 @@ class TimelineFactory {
TimelineService video(String userId) =>
TimelineService(_timelineRepository.video(userId, groupBy));
TimelineService place(String place) =>
TimelineService(_timelineRepository.place(place, groupBy));
}
class TimelineService {
@@ -6,6 +6,7 @@ import 'package:worker_manager/worker_manager.dart';
class BackgroundSyncManager {
Cancelable<void>? _syncTask;
Cancelable<void>? _syncWebsocketTask;
Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _hashTask;
@@ -20,6 +21,12 @@ class BackgroundSyncManager {
_syncTask?.cancel();
_syncTask = null;
if (_syncWebsocketTask != null) {
futures.add(_syncWebsocketTask!.future);
}
_syncWebsocketTask?.cancel();
_syncWebsocketTask = null;
return Future.wait(futures);
}
@@ -72,4 +79,19 @@ class BackgroundSyncManager {
_syncTask = null;
});
}
Future<void> syncWebsocketBatch(List<dynamic> batchData) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
}
_syncWebsocketTask = runInIsolateGentle(
computation: (ref) => ref
.read(syncStreamServiceProvider)
.handleWsAssetUploadReadyV1Batch(batchData),
);
return _syncWebsocketTask!.whenComplete(() {
_syncWebsocketTask = null;
});
}
}
@@ -17,6 +17,7 @@ mergedAsset: SELECT * FROM
rae.thumb_hash,
rae.checksum,
rae.owner_id,
rae.live_photo_video_id,
0 as orientation
FROM
remote_asset_entity rae
@@ -39,6 +40,7 @@ mergedAsset: SELECT * FROM
NULL as thumb_hash,
lae.checksum,
NULL as owner_id,
NULL as live_photo_video_id,
lae.orientation
FROM
local_asset_entity lae
+4 -1
View File
@@ -18,7 +18,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, 0 AS orientation FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, lae.orientation FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in var1) i0.Variable<String>($),
...generatedlimit.introducedVariables
@@ -42,6 +42,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
thumbHash: row.readNullable<String>('thumb_hash'),
checksum: row.readNullable<String>('checksum'),
ownerId: row.readNullable<String>('owner_id'),
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
orientation: row.read<int>('orientation'),
));
}
@@ -88,6 +89,7 @@ class MergedAssetResult {
final String? thumbHash;
final String? checksum;
final String? ownerId;
final String? livePhotoVideoId;
final int orientation;
MergedAssetResult({
this.remoteId,
@@ -103,6 +105,7 @@ class MergedAssetResult {
this.thumbHash,
this.checksum,
this.ownerId,
this.livePhotoVideoId,
required this.orientation,
});
}
@@ -30,6 +30,8 @@ class RemoteAssetEntity extends Table
DateTimeColumn get deletedAt => dateTime().nullable()();
TextColumn get livePhotoVideoId => text().nullable()();
IntColumn get visibility => intEnum<AssetVisibility>()();
@override
@@ -51,6 +53,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
width: width,
thumbHash: thumbHash,
visibility: visibility,
livePhotoVideoId: livePhotoVideoId,
localId: null,
);
}
@@ -27,6 +27,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId,
required i2.AssetVisibility visibility,
});
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
@@ -45,6 +46,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId,
i0.Value<i2.AssetVisibility> visibility,
});
@@ -134,6 +136,10 @@ class $$RemoteAssetEntityTableFilterComposer
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnWithTypeConverterFilters<i2.AssetVisibility, i2.AssetVisibility, int>
get visibility => $composableBuilder(
column: $table.visibility,
@@ -217,6 +223,10 @@ class $$RemoteAssetEntityTableOrderingComposer
column: $table.deletedAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get visibility => $composableBuilder(
column: $table.visibility,
builder: (column) => i0.ColumnOrderings(column));
@@ -292,6 +302,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<DateTime> get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
i0.GeneratedColumn<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int> get visibility =>
$composableBuilder(
column: $table.visibility, builder: (column) => column);
@@ -358,6 +371,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(),
}) =>
i1.RemoteAssetEntityCompanion(
@@ -375,6 +389,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
),
createCompanionCallback: ({
@@ -392,6 +407,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
}) =>
i1.RemoteAssetEntityCompanion.insert(
@@ -409,6 +425,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
),
withReferenceMapper: (p0) => p0
@@ -573,6 +590,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
late final i0.GeneratedColumn<DateTime> deletedAt =
i0.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
static const i0.VerificationMeta _livePhotoVideoIdMeta =
const i0.VerificationMeta('livePhotoVideoId');
@override
late final i0.GeneratedColumn<String> livePhotoVideoId =
i0.GeneratedColumn<String>('live_photo_video_id', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int>
visibility = i0.GeneratedColumn<int>('visibility', aliasedName, false,
@@ -595,6 +618,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
localDateTime,
thumbHash,
deletedAt,
livePhotoVideoId,
visibility
];
@override
@@ -673,6 +697,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
context.handle(_deletedAtMeta,
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta));
}
if (data.containsKey('live_photo_video_id')) {
context.handle(
_livePhotoVideoIdMeta,
livePhotoVideoId.isAcceptableOrUnknown(
data['live_photo_video_id']!, _livePhotoVideoIdMeta));
}
return context;
}
@@ -712,6 +742,9 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
.read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']),
deletedAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']),
livePhotoVideoId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}live_photo_video_id']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!),
@@ -750,6 +783,7 @@ class RemoteAssetEntityData extends i0.DataClass
final DateTime? localDateTime;
final String? thumbHash;
final DateTime? deletedAt;
final String? livePhotoVideoId;
final i2.AssetVisibility visibility;
const RemoteAssetEntityData(
{required this.name,
@@ -766,6 +800,7 @@ class RemoteAssetEntityData extends i0.DataClass
this.localDateTime,
this.thumbHash,
this.deletedAt,
this.livePhotoVideoId,
required this.visibility});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -799,6 +834,9 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
}
if (!nullToAbsent || livePhotoVideoId != null) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId);
}
{
map['visibility'] = i0.Variable<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility));
@@ -825,6 +863,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']),
thumbHash: serializer.fromJson<String?>(json['thumbHash']),
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility
.fromJson(serializer.fromJson<int>(json['visibility'])),
);
@@ -848,6 +887,7 @@ class RemoteAssetEntityData extends i0.DataClass
'localDateTime': serializer.toJson<DateTime?>(localDateTime),
'thumbHash': serializer.toJson<String?>(thumbHash),
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
'visibility': serializer.toJson<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)),
};
@@ -868,6 +908,7 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i2.AssetVisibility? visibility}) =>
i1.RemoteAssetEntityData(
name: name ?? this.name,
@@ -887,6 +928,9 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime.present ? localDateTime.value : this.localDateTime,
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
livePhotoVideoId: livePhotoVideoId.present
? livePhotoVideoId.value
: this.livePhotoVideoId,
visibility: visibility ?? this.visibility,
);
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
@@ -910,6 +954,9 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime,
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
livePhotoVideoId: data.livePhotoVideoId.present
? data.livePhotoVideoId.value
: this.livePhotoVideoId,
visibility:
data.visibility.present ? data.visibility.value : this.visibility,
);
@@ -932,6 +979,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility')
..write(')'))
.toString();
@@ -953,6 +1001,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime,
thumbHash,
deletedAt,
livePhotoVideoId,
visibility);
@override
bool operator ==(Object other) =>
@@ -972,6 +1021,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.localDateTime == this.localDateTime &&
other.thumbHash == this.thumbHash &&
other.deletedAt == this.deletedAt &&
other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility);
}
@@ -991,6 +1041,7 @@ class RemoteAssetEntityCompanion
final i0.Value<DateTime?> localDateTime;
final i0.Value<String?> thumbHash;
final i0.Value<DateTime?> deletedAt;
final i0.Value<String?> livePhotoVideoId;
final i0.Value<i2.AssetVisibility> visibility;
const RemoteAssetEntityCompanion({
this.name = const i0.Value.absent(),
@@ -1007,6 +1058,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
this.visibility = const i0.Value.absent(),
});
RemoteAssetEntityCompanion.insert({
@@ -1024,6 +1076,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
}) : name = i0.Value(name),
type = i0.Value(type),
@@ -1046,6 +1099,7 @@ class RemoteAssetEntityCompanion
i0.Expression<DateTime>? localDateTime,
i0.Expression<String>? thumbHash,
i0.Expression<DateTime>? deletedAt,
i0.Expression<String>? livePhotoVideoId,
i0.Expression<int>? visibility,
}) {
return i0.RawValuesInsertable({
@@ -1063,6 +1117,7 @@ class RemoteAssetEntityCompanion
if (localDateTime != null) 'local_date_time': localDateTime,
if (thumbHash != null) 'thumb_hash': thumbHash,
if (deletedAt != null) 'deleted_at': deletedAt,
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
if (visibility != null) 'visibility': visibility,
});
}
@@ -1082,6 +1137,7 @@ class RemoteAssetEntityCompanion
i0.Value<DateTime?>? localDateTime,
i0.Value<String?>? thumbHash,
i0.Value<DateTime?>? deletedAt,
i0.Value<String?>? livePhotoVideoId,
i0.Value<i2.AssetVisibility>? visibility}) {
return i1.RemoteAssetEntityCompanion(
name: name ?? this.name,
@@ -1098,6 +1154,7 @@ class RemoteAssetEntityCompanion
localDateTime: localDateTime ?? this.localDateTime,
thumbHash: thumbHash ?? this.thumbHash,
deletedAt: deletedAt ?? this.deletedAt,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
visibility: visibility ?? this.visibility,
);
}
@@ -1148,6 +1205,9 @@ class RemoteAssetEntityCompanion
if (deletedAt.present) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
}
if (livePhotoVideoId.present) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId.value);
}
if (visibility.present) {
map['visibility'] = i0.Variable<int>(i1
.$RemoteAssetEntityTable.$convertervisibility
@@ -1173,6 +1233,7 @@ class RemoteAssetEntityCompanion
..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility')
..write(')'))
.toString();
@@ -8,14 +8,16 @@ class UserMetadataEntity extends Table with DriftDefaultsMixin {
TextColumn get userId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get preferences => text().map(userPreferenceConverter)();
IntColumn get key => intEnum<UserMetadataKey>()();
BlobColumn get value => blob().map(userMetadataConverter)();
@override
Set<Column> get primaryKey => {userId};
Set<Column> get primaryKey => {userId, key};
}
final JsonTypeConverter2<UserPreferences, String, Object?>
userPreferenceConverter = TypeConverter.json2(
fromJson: (json) => UserPreferences.fromMap(json as Map<String, Object?>),
toJson: (pref) => pref.toMap(),
final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?>
userMetadataConverter = TypeConverter.jsonb(
fromJson: (json) => json as Map<String, Object?>,
);
@@ -4,21 +4,24 @@ import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2;
import 'dart:typed_data' as i3;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'
as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i4;
import 'package:drift/internal/modular.dart' as i5;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$UserMetadataEntityTableCreateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
required String userId,
required i2.UserPreferences preferences,
required i2.UserMetadataKey key,
required Map<String, Object?> value,
});
typedef $$UserMetadataEntityTableUpdateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
i0.Value<String> userId,
i0.Value<i2.UserPreferences> preferences,
i0.Value<i2.UserMetadataKey> key,
i0.Value<Map<String, Object?>> value,
});
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
@@ -28,26 +31,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
$$UserMetadataEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
i6.ReadDatabaseContainer(db)
.resultSet<i1.$UserMetadataEntityTable>(
'user_metadata_entity')
.userId,
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.id));
i4.$$UserEntityTableProcessedTableManager get userId {
i5.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<String>('user_id')!;
final manager = i4
final manager = i5
.$$UserEntityTableTableManager(
$_db,
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_userIdTable($_db));
if (item == null) return manager;
@@ -65,26 +68,31 @@ class $$UserMetadataEntityTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnWithTypeConverterFilters<i2.UserPreferences, i2.UserPreferences,
String>
get preferences => $composableBuilder(
column: $table.preferences,
i0.ColumnWithTypeConverterFilters<i2.UserMetadataKey, i2.UserMetadataKey, int>
get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i4.$$UserEntityTableFilterComposer get userId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
i0.ColumnWithTypeConverterFilters<Map<String, Object?>, Map<String, Object>,
i3.Uint8List>
get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i5.$$UserEntityTableFilterComposer get userId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableFilterComposer(
i5.$$UserEntityTableFilterComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -103,24 +111,26 @@ class $$UserMetadataEntityTableOrderingComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get preferences => $composableBuilder(
column: $table.preferences,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get key => $composableBuilder(
column: $table.key, builder: (column) => i0.ColumnOrderings(column));
i4.$$UserEntityTableOrderingComposer get userId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i0.ColumnOrderings<i3.Uint8List> get value => $composableBuilder(
column: $table.value, builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get userId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableOrderingComposer(
i5.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -139,24 +149,27 @@ class $$UserMetadataEntityTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
get preferences => $composableBuilder(
column: $table.preferences, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> get key =>
$composableBuilder(column: $table.key, builder: (column) => column);
i4.$$UserEntityTableAnnotationComposer get userId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i0.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
get value =>
$composableBuilder(column: $table.value, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get userId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i4.$$UserEntityTableAnnotationComposer(
i5.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -193,19 +206,23 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager<
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> userId = const i0.Value.absent(),
i0.Value<i2.UserPreferences> preferences = const i0.Value.absent(),
i0.Value<i2.UserMetadataKey> key = const i0.Value.absent(),
i0.Value<Map<String, Object?>> value = const i0.Value.absent(),
}) =>
i1.UserMetadataEntityCompanion(
userId: userId,
preferences: preferences,
key: key,
value: value,
),
createCompanionCallback: ({
required String userId,
required i2.UserPreferences preferences,
required i2.UserMetadataKey key,
required Map<String, Object?> value,
}) =>
i1.UserMetadataEntityCompanion.insert(
userId: userId,
preferences: preferences,
key: key,
value: value,
),
withReferenceMapper: (p0) => p0
.map((e) => (
@@ -266,7 +283,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager
i1.UserMetadataEntityData,
i0.PrefetchHooks Function({bool userId})>;
class $UserMetadataEntityTable extends i3.UserMetadataEntity
class $UserMetadataEntityTable extends i4.UserMetadataEntity
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@@ -282,14 +299,20 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@override
late final i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
preferences = i0.GeneratedColumn<String>(
'preferences', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true)
.withConverter<i2.UserPreferences>(
i1.$UserMetadataEntityTable.$converterpreferences);
late final i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> key =
i0.GeneratedColumn<int>('key', aliasedName, false,
type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.UserMetadataKey>(
i1.$UserMetadataEntityTable.$converterkey);
@override
List<i0.GeneratedColumn> get $columns => [userId, preferences];
late final i0
.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
value = i0.GeneratedColumn<i3.Uint8List>('value', aliasedName, false,
type: i0.DriftSqlType.blob, requiredDuringInsert: true)
.withConverter<Map<String, Object?>>(
i1.$UserMetadataEntityTable.$convertervalue);
@override
List<i0.GeneratedColumn> get $columns => [userId, key, value];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@@ -311,7 +334,7 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {userId};
Set<i0.GeneratedColumn> get $primaryKey => {userId, key};
@override
i1.UserMetadataEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
@@ -319,9 +342,12 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
return i1.UserMetadataEntityData(
userId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!,
preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
key: i1.$UserMetadataEntityTable.$converterkey.fromSql(attachedDatabase
.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}key'])!),
value: i1.$UserMetadataEntityTable.$convertervalue.fromSql(
attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}value'])!),
);
}
@@ -330,8 +356,11 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
return $UserMetadataEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.UserPreferences, String, Object?>
$converterpreferences = i3.userPreferenceConverter;
static i0.JsonTypeConverter2<i2.UserMetadataKey, int, int> $converterkey =
const i0.EnumIndexConverter<i2.UserMetadataKey>(
i2.UserMetadataKey.values);
static i0.JsonTypeConverter2<Map<String, Object?>, i3.Uint8List, Object?>
$convertervalue = i4.userMetadataConverter;
@override
bool get withoutRowId => true;
@override
@@ -341,16 +370,21 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
class UserMetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.UserMetadataEntityData> {
final String userId;
final i2.UserPreferences preferences;
final i2.UserMetadataKey key;
final Map<String, Object?> value;
const UserMetadataEntityData(
{required this.userId, required this.preferences});
{required this.userId, required this.key, required this.value});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['user_id'] = i0.Variable<String>(userId);
{
map['preferences'] = i0.Variable<String>(
i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
map['key'] = i0.Variable<int>(
i1.$UserMetadataEntityTable.$converterkey.toSql(key));
}
{
map['value'] = i0.Variable<i3.Uint8List>(
i1.$UserMetadataEntityTable.$convertervalue.toSql(value));
}
return map;
}
@@ -360,8 +394,10 @@ class UserMetadataEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserMetadataEntityData(
userId: serializer.fromJson<String>(json['userId']),
preferences: i1.$UserMetadataEntityTable.$converterpreferences
.fromJson(serializer.fromJson<Object?>(json['preferences'])),
key: i1.$UserMetadataEntityTable.$converterkey
.fromJson(serializer.fromJson<int>(json['key'])),
value: i1.$UserMetadataEntityTable.$convertervalue
.fromJson(serializer.fromJson<Object?>(json['value'])),
);
}
@override
@@ -369,24 +405,28 @@ class UserMetadataEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<String>(userId),
'preferences': serializer.toJson<Object?>(i1
.$UserMetadataEntityTable.$converterpreferences
.toJson(preferences)),
'key': serializer
.toJson<int>(i1.$UserMetadataEntityTable.$converterkey.toJson(key)),
'value': serializer.toJson<Object?>(
i1.$UserMetadataEntityTable.$convertervalue.toJson(value)),
};
}
i1.UserMetadataEntityData copyWith(
{String? userId, i2.UserPreferences? preferences}) =>
{String? userId,
i2.UserMetadataKey? key,
Map<String, Object?>? value}) =>
i1.UserMetadataEntityData(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
key: key ?? this.key,
value: value ?? this.value,
);
UserMetadataEntityData copyWithCompanion(
i1.UserMetadataEntityCompanion data) {
return UserMetadataEntityData(
userId: data.userId.present ? data.userId.value : this.userId,
preferences:
data.preferences.present ? data.preferences.value : this.preferences,
key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value,
);
}
@@ -394,49 +434,60 @@ class UserMetadataEntityData extends i0.DataClass
String toString() {
return (StringBuffer('UserMetadataEntityData(')
..write('userId: $userId, ')
..write('preferences: $preferences')
..write('key: $key, ')
..write('value: $value')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(userId, preferences);
int get hashCode => Object.hash(userId, key, value);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserMetadataEntityData &&
other.userId == this.userId &&
other.preferences == this.preferences);
other.key == this.key &&
other.value == this.value);
}
class UserMetadataEntityCompanion
extends i0.UpdateCompanion<i1.UserMetadataEntityData> {
final i0.Value<String> userId;
final i0.Value<i2.UserPreferences> preferences;
final i0.Value<i2.UserMetadataKey> key;
final i0.Value<Map<String, Object?>> value;
const UserMetadataEntityCompanion({
this.userId = const i0.Value.absent(),
this.preferences = const i0.Value.absent(),
this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(),
});
UserMetadataEntityCompanion.insert({
required String userId,
required i2.UserPreferences preferences,
required i2.UserMetadataKey key,
required Map<String, Object?> value,
}) : userId = i0.Value(userId),
preferences = i0.Value(preferences);
key = i0.Value(key),
value = i0.Value(value);
static i0.Insertable<i1.UserMetadataEntityData> custom({
i0.Expression<String>? userId,
i0.Expression<String>? preferences,
i0.Expression<int>? key,
i0.Expression<i3.Uint8List>? value,
}) {
return i0.RawValuesInsertable({
if (userId != null) 'user_id': userId,
if (preferences != null) 'preferences': preferences,
if (key != null) 'key': key,
if (value != null) 'value': value,
});
}
i1.UserMetadataEntityCompanion copyWith(
{i0.Value<String>? userId, i0.Value<i2.UserPreferences>? preferences}) {
{i0.Value<String>? userId,
i0.Value<i2.UserMetadataKey>? key,
i0.Value<Map<String, Object?>>? value}) {
return i1.UserMetadataEntityCompanion(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
key: key ?? this.key,
value: value ?? this.value,
);
}
@@ -446,10 +497,13 @@ class UserMetadataEntityCompanion
if (userId.present) {
map['user_id'] = i0.Variable<String>(userId.value);
}
if (preferences.present) {
map['preferences'] = i0.Variable<String>(i1
.$UserMetadataEntityTable.$converterpreferences
.toSql(preferences.value));
if (key.present) {
map['key'] = i0.Variable<int>(
i1.$UserMetadataEntityTable.$converterkey.toSql(key.value));
}
if (value.present) {
map['value'] = i0.Variable<i3.Uint8List>(
i1.$UserMetadataEntityTable.$convertervalue.toSql(value.value));
}
return map;
}
@@ -458,7 +512,8 @@ class UserMetadataEntityCompanion
String toString() {
return (StringBuffer('UserMetadataEntityCompanion(')
..write('userId: $userId, ')
..write('preferences: $preferences')
..write('key: $key, ')
..write('value: $value')
..write(')'))
.toString();
}
@@ -56,6 +56,42 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
.getSingleOrNull();
}
Future<List<(String, String)>> getPlaces() {
final asset = Subquery(
_db.remoteAssetEntity.select()
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]),
"asset",
);
final query = asset.selectOnly().join([
innerJoin(
_db.remoteExifEntity,
_db.remoteExifEntity.assetId
.equalsExp(asset.ref(_db.remoteAssetEntity.id)),
useColumns: false,
),
])
..addColumns([
_db.remoteExifEntity.city,
_db.remoteExifEntity.assetId,
])
..where(
_db.remoteExifEntity.city.isNotNull() &
asset.ref(_db.remoteAssetEntity.deletedAt).isNull() &
asset
.ref(_db.remoteAssetEntity.visibility)
.equals(AssetVisibility.timeline.index),
)
..groupBy([_db.remoteExifEntity.city])
..orderBy([OrderingTerm.asc(_db.remoteExifEntity.city)]);
return query.map((row) {
final assetId = row.read(_db.remoteExifEntity.assetId);
final city = row.read(_db.remoteExifEntity.city);
return (city!, assetId!);
}).get();
}
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
return _db.batch((batch) async {
for (final id in ids) {
@@ -56,6 +56,7 @@ class SyncApiRepository {
SyncRequestType.memoryToAssetsV1,
SyncRequestType.stacksV1,
SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1,
],
).toJson(),
);
@@ -170,6 +171,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson,
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
};
class _SyncAckV1 {
@@ -4,16 +4,18 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole;
@@ -134,6 +136,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
thumbHash: Value(asset.thumbhash),
deletedAt: Value(asset.deletedAt),
visibility: Value(asset.visibility.toAssetVisibility()),
livePhotoVideoId: Value(asset.livePhotoVideoId),
);
batch.insert(
@@ -460,6 +463,53 @@ class SyncStreamRepository extends DriftDatabaseRepository {
rethrow;
}
}
Future<void> updateUserMetadatasV1(
Iterable<SyncUserMetadataV1> data,
) async {
try {
await _db.batch((batch) {
for (final userMetadata in data) {
final companion = UserMetadataEntityCompanion(
value: Value(userMetadata.value as Map<String, Object?>),
);
batch.insert(
_db.userMetadataEntity,
companion.copyWith(
userId: Value(userMetadata.userId),
key: Value(userMetadata.key.toUserMetadataKey()),
),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: deleteUserMetadatasV1', error, stack);
rethrow;
}
}
Future<void> deleteUserMetadatasV1(
Iterable<SyncUserMetadataDeleteV1> data,
) async {
try {
await _db.batch((batch) {
for (final userMetadata in data) {
batch.delete(
_db.userMetadataEntity,
UserMetadataEntityCompanion(
userId: Value(userMetadata.userId),
key: Value(userMetadata.key.toUserMetadataKey()),
),
);
}
});
} catch (error, stack) {
_logger.severe('Error: deleteUserMetadatasV1', error, stack);
rethrow;
}
}
}
extension on AssetTypeEnum {
@@ -505,6 +555,15 @@ extension on api.AssetVisibility {
};
}
extension on String {
UserMetadataKey toUserMetadataKey() => switch (this) {
"onboarding" => UserMetadataKey.onboarding,
"preferences" => UserMetadataKey.preferences,
"license" => UserMetadataKey.license,
_ => throw Exception('Unknown UserMetadataKey value: $this'),
};
}
extension on String {
Duration? toDuration() {
try {
@@ -87,6 +87,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
height: row.height,
isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds,
livePhotoVideoId: row.livePhotoVideoId,
)
: LocalAsset(
id: row.localId!,
@@ -302,6 +303,84 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
groupBy: groupBy,
);
TimelineQuery place(String place, GroupAssetsBy groupBy) => (
bucketSource: () => _watchPlaceBucket(
place,
groupBy: groupBy,
),
assetSource: (offset, count) => _getPlaceBucketAssets(
place,
offset: offset,
count: count,
),
);
Stream<List<Bucket>> _watchPlaceBucket(
String place, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
// TODO: implement GroupAssetBy for place
throw UnsupportedError(
"GroupAssetsBy.none is not supported for watchPlaceBucket",
);
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..join([
innerJoin(
_db.remoteExifEntity,
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteExifEntity.city.equals(place) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> _getPlaceBucketAssets(
String place, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select().join(
[
innerJoin(
_db.remoteExifEntity,
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
],
)
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteExifEntity.city.equals(place),
)
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get();
}
TimelineQuery _remoteQueryBuilder({
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
GroupAssetsBy groupBy = GroupAssetsBy.day,
@@ -0,0 +1,38 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftUserMetadataRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftUserMetadataRepository(this._db) : super(_db);
Future<List<UserMetadata>> getUserMetadata(String userId) {
final query = _db.userMetadataEntity.select()
..where((e) => e.userId.equals(userId));
return query.map((userMetadata) {
return userMetadata.toDto();
}).get();
}
}
extension on UserMetadataEntityData {
UserMetadata toDto() => switch (key) {
UserMetadataKey.onboarding => UserMetadata(
userId: userId,
key: key,
onboarding: Onboarding.fromMap(value),
),
UserMetadataKey.preferences => UserMetadata(
userId: userId,
key: key,
preferences: Preferences.fromMap(value),
),
UserMetadataKey.license => UserMetadata(
userId: userId,
key: key,
license: License.fromMap(value),
),
};
}
@@ -256,9 +256,7 @@ class _PlacesCollectionCard extends StatelessWidget {
return GestureDetector(
onTap: () => context.pushRoute(
PlacesCollectionRoute(
currentLocation: null,
),
DriftPlaceRoute(currentLocation: null),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -0,0 +1,190 @@
import 'package:auto_route/auto_route.dart';
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/extensions/translate_extensions.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage()
class DriftPlacePage extends StatelessWidget {
const DriftPlacePage({super.key, this.currentLocation});
final LatLng? currentLocation;
@override
Widget build(BuildContext context) {
final ValueNotifier<String?> search = ValueNotifier(null);
return Scaffold(
body: CustomScrollView(
slivers: [
_PlaceSliverAppBar(search: search),
_Map(search: search, currentLocation: currentLocation),
_PlaceList(search: search),
],
),
);
}
}
class _PlaceSliverAppBar extends StatelessWidget {
const _PlaceSliverAppBar({required this.search});
final ValueNotifier<String?> search;
@override
Widget build(BuildContext context) {
final searchFocusNode = FocusNode();
return SliverAppBar(
floating: true,
pinned: true,
snap: false,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
automaticallyImplyLeading: search.value == null,
centerTitle: true,
title: search.value != null
? SearchField(
focusNode: searchFocusNode,
onTapOutside: (_) => searchFocusNode.unfocus(),
onChanged: (value) => search.value = value,
filled: true,
hintText: 'filter_places'.t(context: context),
autofocus: true,
)
: Text('places'.t(context: context)),
actions: [
IconButton(
icon: Icon(search.value != null ? Icons.close : Icons.search),
onPressed: () {
search.value = search.value == null ? '' : null;
},
),
],
);
}
}
class _Map extends StatelessWidget {
const _Map({required this.search, this.currentLocation});
final ValueNotifier<String?> search;
final LatLng? currentLocation;
@override
Widget build(BuildContext context) {
return search.value == null
? SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverToBoxAdapter(
child: SizedBox(
height: 200,
width: context.width,
// TODO: migrate to DriftMapRoute after merging #19898
child: MapThumbnail(
onTap: (_, __) => context
.pushRoute(MapRoute(initialLocation: currentLocation)),
zoom: 8,
centre: currentLocation ??
const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode:
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
),
),
),
)
: const SizedBox.shrink();
}
}
class _PlaceList extends ConsumerWidget {
const _PlaceList({required this.search});
final ValueNotifier<String?> search;
@override
Widget build(BuildContext context, WidgetRef ref) {
final places = ref.watch(placesProvider);
return places.when(
loading: () => const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
),
),
error: (error, stack) => SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
'Error loading places: $error, stack: $stack',
style: TextStyle(
color: context.colorScheme.error,
),
),
),
),
),
data: (places) {
if (search.value != null) {
places = places.where((place) {
return place.$1.toLowerCase().contains(search.value!.toLowerCase());
}).toList();
}
return SliverList.builder(
itemCount: places.length,
itemBuilder: (context, index) {
final place = places[index];
return _PlaceTile(place: place);
},
);
},
);
}
}
class _PlaceTile extends StatelessWidget {
const _PlaceTile({required this.place});
final (String, String) place;
@override
Widget build(BuildContext context) {
return LargeLeadingTile(
onTap: () => context.pushRoute(DriftPlaceDetailRoute(place: place.$1)),
title: Text(
place.$1,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
leading: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
child: Thumbnail(
size: const Size(80, 80),
fit: BoxFit.cover,
remoteId: place.$2,
),
),
);
}
}
@@ -0,0 +1,38 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage()
class DriftPlaceDetailPage extends StatelessWidget {
final String place;
const DriftPlaceDetailPage({
super.key,
required this.place,
});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService =
ref.watch(timelineFactoryProvider).place(place);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(
title: place,
icon: Icons.location_on,
),
),
);
}
}
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
class MotionPhotoActionButton extends ConsumerWidget {
const MotionPhotoActionButton({super.key, this.menuItem = true});
final bool menuItem;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPlaying = ref.watch(isPlayingMotionVideoProvider);
return BaseActionButton(
iconData: isPlaying
? Icons.motion_photos_pause_outlined
: Icons.play_circle_outline_rounded,
label: "play_motion_photo".t(context: context),
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
menuItem: menuItem,
);
}
}
@@ -15,6 +15,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widg
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
@@ -165,7 +166,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
void _onAssetChanged(int index) {
final asset = ref.read(timelineServiceProvider).getAsset(index);
ref.read(currentAssetNotifier.notifier).setAsset(asset);
if (asset.isVideo) {
if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset();
ref.read(videoPlayerControlsProvider.notifier).pause();
}
@@ -473,11 +474,16 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
}
}
void _onLongPress(_, __, ___) {
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
}
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
scaffoldContext ??= ctx;
final asset = ref.read(timelineServiceProvider).getAsset(index);
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
if (asset.isImage) {
if (asset.isImage && !isPlayingMotionVideo) {
return _imageBuilder(ctx, asset);
}
@@ -500,6 +506,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
onDragUpdate: _onDragUpdate,
onDragEnd: _onDragEnd,
onTapDown: _onTapDown,
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
errorBuilder: (_, __, ___) => Container(
width: ctx.width,
height: ctx.height,
@@ -561,6 +568,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
ref.watch(isPlayingMotionVideoProvider);
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
// Issue: https://github.com/flutter/flutter/issues/109037
@@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
@@ -44,6 +45,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
source: ActionSource.viewer,
menuItem: true,
),
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
const _KebabMenu(),
];
@@ -270,10 +270,7 @@ class NativeVideoViewer extends HookConsumerWidget {
return;
}
if (videoController.playbackInfo?.status == PlaybackStatus.stopped &&
!ref
.read(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.loopVideo)) {
if (videoController.playbackInfo?.status == PlaybackStatus.stopped) {
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
}
}
@@ -310,7 +307,7 @@ class NativeVideoViewer extends HookConsumerWidget {
final loopVideo = ref
.read(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.loopVideo);
nc.setLoop(loopVideo);
nc.setLoop(!asset.isMotionPhoto && loopVideo);
controller.value = nc;
Timer(const Duration(milliseconds: 200), checkIfBuffering);
@@ -10,6 +10,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@@ -171,6 +172,7 @@ class _AssetTileWidget extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
} else {
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
ctx.pushRoute(
AssetViewerRoute(
initialIndex: assetIndex,
@@ -18,3 +18,10 @@ final assetServiceProvider = Provider(
localAssetRepository: ref.watch(localAssetRepository),
),
);
final placesProvider = FutureProvider<List<(String, String)>>(
(ref) => AssetService(
remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider),
localAssetRepository: ref.watch(localAssetRepository),
).getPlaces(),
);
@@ -0,0 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final userMetadataRepository = Provider<DriftUserMetadataRepository>(
(ref) => DriftUserMetadataRepository(ref.watch(driftProvider)),
);
+41 -1
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
@@ -10,6 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
@@ -106,6 +108,18 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
final Debouncer _debounce =
Debouncer(interval: const Duration(milliseconds: 500));
final Debouncer _batchDebouncer = Debouncer(
interval: const Duration(seconds: 5),
maxWaitTime: const Duration(seconds: 10),
);
final List<dynamic> _batchedAssetUploadReady = [];
@override
void dispose() {
_batchDebouncer.dispose();
super.dispose();
}
/// Connects websocket to server unless already connected
void connect() {
if (state.isConnected) return;
@@ -171,6 +185,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_asset_stack_update', _handleServerUpdates);
socket.on('on_asset_hidden', _handleOnAssetHidden);
socket.on('on_new_release', _handleReleaseUpdates);
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}
@@ -180,6 +195,8 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
void disconnect() {
debugPrint("Attempting to disconnect from websocket");
_batchedAssetUploadReady.clear();
var socket = state.socket?.disconnect();
if (socket?.disconnected == true) {
@@ -288,7 +305,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
}
void handlePendingChanges() async {
Future<void> handlePendingChanges() async {
await _handlePendingUploaded();
await _handlePendingDeletes();
await _handlingPendingHidden();
@@ -347,6 +364,29 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
.read(serverInfoProvider.notifier)
.handleNewRelease(serverVersion, releaseVersion);
}
void _handleSyncAssetUploadReady(dynamic data) {
_batchedAssetUploadReady.add(data);
_batchDebouncer.run(_processBatchedAssetUploadReady);
}
void _processBatchedAssetUploadReady() {
if (_batchedAssetUploadReady.isEmpty) {
return;
}
try {
unawaited(
_ref
.read(backgroundSyncProvider)
.syncWebsocketBatch(_batchedAssetUploadReady.toList()),
);
} catch (error) {
_log.severe("Error processing batched AssetUploadReadyV1 events: $error");
}
_batchedAssetUploadReady.clear();
}
}
final websocketProvider =
+10 -1
View File
@@ -72,6 +72,8 @@ import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
@@ -453,7 +455,14 @@ class AppRouter extends RootStackRouter {
page: DriftCreateAlbumRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftPlaceRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftPlaceDetailRoute.page,
guards: [_authGuard, _duplicateGuard],
),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),
+79
View File
@@ -853,6 +853,85 @@ class DriftPartnerDetailRouteArgs {
}
}
/// generated route for
/// [DriftPlaceDetailPage]
class DriftPlaceDetailRoute extends PageRouteInfo<DriftPlaceDetailRouteArgs> {
DriftPlaceDetailRoute({
Key? key,
required String place,
List<PageRouteInfo>? children,
}) : super(
DriftPlaceDetailRoute.name,
args: DriftPlaceDetailRouteArgs(key: key, place: place),
initialChildren: children,
);
static const String name = 'DriftPlaceDetailRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<DriftPlaceDetailRouteArgs>();
return DriftPlaceDetailPage(key: args.key, place: args.place);
},
);
}
class DriftPlaceDetailRouteArgs {
const DriftPlaceDetailRouteArgs({this.key, required this.place});
final Key? key;
final String place;
@override
String toString() {
return 'DriftPlaceDetailRouteArgs{key: $key, place: $place}';
}
}
/// generated route for
/// [DriftPlacePage]
class DriftPlaceRoute extends PageRouteInfo<DriftPlaceRouteArgs> {
DriftPlaceRoute({
Key? key,
LatLng? currentLocation,
List<PageRouteInfo>? children,
}) : super(
DriftPlaceRoute.name,
args: DriftPlaceRouteArgs(key: key, currentLocation: currentLocation),
initialChildren: children,
);
static const String name = 'DriftPlaceRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<DriftPlaceRouteArgs>(
orElse: () => const DriftPlaceRouteArgs(),
);
return DriftPlacePage(
key: args.key,
currentLocation: args.currentLocation,
);
},
);
}
class DriftPlaceRouteArgs {
const DriftPlaceRouteArgs({this.key, this.currentLocation});
final Key? key;
final LatLng? currentLocation;
@override
String toString() {
return 'DriftPlaceRouteArgs{key: $key, currentLocation: $currentLocation}';
}
}
/// generated route for
/// [DriftRecentlyTakenPage]
class DriftRecentlyTakenRoute extends PageRouteInfo<void> {
@@ -482,10 +482,10 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
builder: (context, child) {
return Transform.scale(
scale: _zoomAnimation.value,
filterQuality: FilterQuality.low,
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
child: Transform.translate(
offset: _panAnimation.value,
filterQuality: FilterQuality.low,
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
child: Stack(
fit: StackFit.expand,
children: [
@@ -101,6 +101,10 @@ void main() {
debugLabel: any(named: 'debugLabel'),
),
).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.updateUserMetadatasV1(any()))
.thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteUserMetadatasV1(any()))
.thenAnswer(successHandler);
sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo,
+1904 -881
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -44,18 +44,18 @@
"@nestjs/swagger": "^11.0.2",
"@nestjs/websockets": "^11.0.4",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.60.0",
"@opentelemetry/auto-instrumentations-node": "^0.62.0",
"@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/exporter-prometheus": "^0.202.0",
"@opentelemetry/instrumentation-http": "^0.202.0",
"@opentelemetry/instrumentation-ioredis": "^0.50.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.48.0",
"@opentelemetry/instrumentation-pg": "^0.54.0",
"@opentelemetry/exporter-prometheus": "^0.203.0",
"@opentelemetry/instrumentation-http": "^0.203.0",
"@opentelemetry/instrumentation-ioredis": "^0.51.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.49.0",
"@opentelemetry/instrumentation-pg": "^0.55.0",
"@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-metrics": "^2.0.1",
"@opentelemetry/sdk-node": "^0.202.0",
"@opentelemetry/sdk-node": "^0.203.0",
"@opentelemetry/semantic-conventions": "^1.34.0",
"@react-email/components": "^0.1.0",
"@react-email/components": "^0.2.0",
"@react-email/render": "^1.1.2",
"@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.0",
+2 -2
View File
@@ -73,11 +73,11 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
);
this.eventRepository.setup({ services });
await this.eventRepository.emit('app.bootstrap');
await this.eventRepository.emit('AppBootstrap');
}
async onModuleDestroy() {
await this.eventRepository.emit('app.shutdown');
await this.eventRepository.emit('AppShutdown');
await teardownTelemetry();
}
}
@@ -59,6 +59,13 @@ describe(APIKeyController.name, () => {
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
});
it('should allow updating just the name', async () => {
const { status } = await request(ctx.getHttpServer())
.put(`/api-keys/${factory.uuid()}`)
.send({ name: 'new name' });
expect(status).toBe(200);
});
});
describe('DELETE /api-keys/:id', () => {
+6 -11
View File
@@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { Activity } from 'src/database';
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
import { Optional, ValidateUUID } from 'src/validation';
import { ValidateEnum, ValidateUUID } from 'src/validation';
export enum ReactionType {
COMMENT = 'comment',
@@ -19,7 +19,7 @@ export type MaybeDuplicate<T> = { duplicate: boolean; value: T };
export class ActivityResponseDto {
id!: string;
createdAt!: Date;
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
@ValidateEnum({ enum: ReactionType, name: 'ReactionType' })
type!: ReactionType;
user!: UserResponseDto;
assetId!: string | null;
@@ -43,14 +43,10 @@ export class ActivityDto {
}
export class ActivitySearchDto extends ActivityDto {
@IsEnum(ReactionType)
@Optional()
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', optional: true })
type?: ReactionType;
@IsEnum(ReactionLevel)
@Optional()
@ApiProperty({ enumName: 'ReactionLevel', enum: ReactionLevel })
@ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', optional: true })
level?: ReactionLevel;
@ValidateUUID({ optional: true })
@@ -60,8 +56,7 @@ export class ActivitySearchDto extends ActivityDto {
const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT;
export class ActivityCreateDto extends ActivityDto {
@IsEnum(ReactionType)
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
@ValidateEnum({ enum: ReactionType, name: 'ReactionType' })
type!: ReactionType;
@ValidateIf(isComment)
+8 -14
View File
@@ -1,13 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator';
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
import _ from 'lodash';
import { AlbumUser, AuthSharedLink, User } from 'src/database';
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AlbumUserRole, AssetOrder } from 'src/enum';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
export class AlbumInfoDto {
@ValidateBoolean({ optional: true })
@@ -18,8 +18,7 @@ export class AlbumUserAddDto {
@ValidateUUID()
userId!: string;
@IsEnum(AlbumUserRole)
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole', default: AlbumUserRole.EDITOR })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.EDITOR })
role?: AlbumUserRole;
}
@@ -32,8 +31,7 @@ export class AlbumUserCreateDto {
@ValidateUUID()
userId!: string;
@IsEnum(AlbumUserRole)
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
role!: AlbumUserRole;
}
@@ -71,9 +69,7 @@ export class UpdateAlbumDto {
@ValidateBoolean({ optional: true })
isActivityEnabled?: boolean;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
order?: AssetOrder;
}
@@ -107,14 +103,13 @@ export class AlbumStatisticsResponseDto {
}
export class UpdateAlbumUserDto {
@IsEnum(AlbumUserRole)
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
role!: AlbumUserRole;
}
export class AlbumUserResponseDto {
user!: UserResponseDto;
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
role!: AlbumUserRole;
}
@@ -137,8 +132,7 @@ export class AlbumResponseDto {
startDate?: Date;
endDate?: Date;
isActivityEnabled!: boolean;
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
order?: AssetOrder;
}
+5 -9
View File
@@ -1,15 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator';
import { Permission } from 'src/enum';
import { Optional } from 'src/validation';
import { Optional, ValidateEnum } from 'src/validation';
export class APIKeyCreateDto {
@IsString()
@IsNotEmpty()
@Optional()
name?: string;
@IsEnum(Permission, { each: true })
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
@ArrayMinSize(1)
permissions!: Permission[];
}
@@ -20,9 +18,7 @@ export class APIKeyUpdateDto {
@IsNotEmpty()
name?: string;
@Optional()
@IsEnum(Permission, { each: true })
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
@ValidateEnum({ enum: Permission, name: 'Permission', each: true, optional: true })
@ArrayMinSize(1)
permissions?: Permission[];
}
@@ -37,6 +33,6 @@ export class APIKeyResponseDto {
name!: string;
createdAt!: Date;
updatedAt!: Date;
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
permissions!: Permission[];
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ValidateEnum } from 'src/validation';
export enum AssetMediaStatus {
CREATED = 'created',
@@ -6,7 +6,7 @@ export enum AssetMediaStatus {
DUPLICATE = 'duplicate',
}
export class AssetMediaResponseDto {
@ApiProperty({ enum: AssetMediaStatus, enumName: 'AssetMediaStatus' })
@ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus' })
status!: AssetMediaStatus;
id!: string;
}
+4 -6
View File
@@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { AssetVisibility } from 'src/enum';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
export enum AssetMediaSize {
/**
@@ -15,9 +15,7 @@ export enum AssetMediaSize {
}
export class AssetMediaOptionsDto {
@Optional()
@IsEnum(AssetMediaSize)
@ApiProperty({ enumName: 'AssetMediaSize', enum: AssetMediaSize })
@ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true })
size?: AssetMediaSize;
}
@@ -60,7 +58,7 @@ export class AssetMediaCreateDto extends AssetMediaBase {
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateAssetVisibility({ optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
visibility?: AssetVisibility;
@ValidateUUID({ optional: true })
+3 -2
View File
@@ -15,10 +15,11 @@ import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types';
import { ValidateEnum } from 'src/validation';
export class SanitizedAssetResponseDto {
id!: string;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' })
type!: AssetType;
thumbhash!: string | null;
originalMimeType?: string;
@@ -72,7 +73,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
isArchived!: boolean;
isTrashed!: boolean;
isOffline!: boolean;
@ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility' })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' })
visibility!: AssetVisibility;
exifInfo?: ExifResponseDto;
tags?: TagResponseDto[];
+4 -6
View File
@@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsDateString,
IsEnum,
IsInt,
IsLatitude,
IsLongitude,
@@ -16,7 +15,7 @@ import {
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AssetType, AssetVisibility } from 'src/enum';
import { AssetStats } from 'src/repositories/asset.repository';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
export class DeviceIdDto {
@IsNotEmpty()
@@ -32,7 +31,7 @@ export class UpdateAssetBase {
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateAssetVisibility({ optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
visibility?: AssetVisibility;
@Optional()
@@ -99,13 +98,12 @@ export enum AssetJobName {
}
export class AssetJobsDto extends AssetIdsDto {
@ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName })
@IsEnum(AssetJobName)
@ValidateEnum({ enum: AssetJobName, name: 'AssetJobName' })
name!: AssetJobName;
}
export class AssetStatsDto {
@ValidateAssetVisibility({ optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
visibility?: AssetVisibility;
@ValidateBoolean({ optional: true })
+4 -10
View File
@@ -1,19 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { JobCommand, ManualJobName, QueueName } from 'src/enum';
import { ValidateBoolean } from 'src/validation';
import { ValidateBoolean, ValidateEnum } from 'src/validation';
export class JobIdParamDto {
@IsNotEmpty()
@IsEnum(QueueName)
@ApiProperty({ type: String, enum: QueueName, enumName: 'JobName' })
@ValidateEnum({ enum: QueueName, name: 'JobName' })
id!: QueueName;
}
export class JobCommandDto {
@IsNotEmpty()
@IsEnum(JobCommand)
@ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' })
@ValidateEnum({ enum: JobCommand, name: 'JobCommand' })
command!: JobCommand;
@ValidateBoolean({ optional: true })
@@ -21,8 +16,7 @@ export class JobCommandDto {
}
export class JobCreateDto {
@IsEnum(ManualJobName)
@ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' })
@ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' })
name!: ManualJobName;
}
+5 -8
View File
@@ -1,11 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
import { Memory } from 'src/database';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
class MemoryBaseDto {
@ValidateBoolean({ optional: true })
@@ -16,9 +16,7 @@ class MemoryBaseDto {
}
export class MemorySearchDto {
@Optional()
@IsEnum(MemoryType)
@ApiProperty({ enum: MemoryType, enumName: 'MemoryType' })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType', optional: true })
type?: MemoryType;
@ValidateDate({ optional: true })
@@ -45,8 +43,7 @@ export class MemoryUpdateDto extends MemoryBaseDto {
}
export class MemoryCreateDto extends MemoryBaseDto {
@IsEnum(MemoryType)
@ApiProperty({ enum: MemoryType, enumName: 'MemoryType' })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
type!: MemoryType;
@IsObject()
@@ -86,7 +83,7 @@ export class MemoryResponseDto {
showAt?: Date;
hideAt?: Date;
ownerId!: string;
@ApiProperty({ enumName: 'MemoryType', enum: MemoryType })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
type!: MemoryType;
data!: MemoryData;
isSaved!: boolean;
+8 -18
View File
@@ -1,7 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsString } from 'class-validator';
import { IsString } from 'class-validator';
import { NotificationLevel, NotificationType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
export class TestEmailResponseDto {
messageId!: string;
@@ -19,9 +18,9 @@ export class NotificationDto {
id!: string;
@ValidateDate()
createdAt!: Date;
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel' })
level!: NotificationLevel;
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
@ValidateEnum({ enum: NotificationType, name: 'NotificationType' })
type!: NotificationType;
title!: string;
description?: string;
@@ -30,18 +29,13 @@ export class NotificationDto {
}
export class NotificationSearchDto {
@Optional()
@ValidateUUID({ optional: true })
id?: string;
@IsEnum(NotificationLevel)
@Optional()
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true })
level?: NotificationLevel;
@IsEnum(NotificationType)
@Optional()
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true })
type?: NotificationType;
@ValidateBoolean({ optional: true })
@@ -49,14 +43,10 @@ export class NotificationSearchDto {
}
export class NotificationCreateDto {
@Optional()
@IsEnum(NotificationLevel)
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true })
level?: NotificationLevel;
@IsEnum(NotificationType)
@Optional()
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true })
type?: NotificationType;
@IsString()
+2 -3
View File
@@ -1,8 +1,7 @@
import { IsBoolean, IsNotEmpty } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
export class OnboardingDto {
@IsBoolean()
@IsNotEmpty()
@ValidateBoolean()
isOnboarded!: boolean;
}
+3 -4
View File
@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { IsNotEmpty } from 'class-validator';
import { UserResponseDto } from 'src/dtos/user.dto';
import { PartnerDirection } from 'src/repositories/partner.repository';
import { ValidateEnum } from 'src/validation';
export class UpdatePartnerDto {
@IsNotEmpty()
@@ -9,8 +9,7 @@ export class UpdatePartnerDto {
}
export class PartnerSearchDto {
@IsEnum(PartnerDirection)
@ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' })
@ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection' })
direction!: PartnerDirection;
}
+2 -1
View File
@@ -14,6 +14,7 @@ import {
MaxDateString,
Optional,
ValidateBoolean,
ValidateEnum,
ValidateHexColor,
ValidateUUID,
} from 'src/validation';
@@ -137,7 +138,7 @@ export class AssetFaceWithoutPersonResponseDto {
boundingBoxY1!: number;
@ApiProperty({ type: 'integer' })
boundingBoxY2!: number;
@ApiProperty({ enum: SourceType, enumName: 'SourceType' })
@ValidateEnum({ enum: SourceType, name: 'SourceType' })
sourceType?: SourceType;
}
+6 -12
View File
@@ -1,12 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { Place } from 'src/database';
import { PropertyLifecycle } from 'src/decorators';
import { AlbumResponseDto } from 'src/dtos/album.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
class BaseSearchDto {
@ValidateUUID({ optional: true, nullable: true })
@@ -17,9 +17,7 @@ class BaseSearchDto {
@Optional()
deviceId?: string;
@IsEnum(AssetType)
@Optional()
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true })
type?: AssetType;
@ValidateBoolean({ optional: true })
@@ -34,7 +32,7 @@ class BaseSearchDto {
@ValidateBoolean({ optional: true })
isOffline?: boolean;
@ValidateAssetVisibility({ optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
visibility?: AssetVisibility;
@ValidateDate({ optional: true })
@@ -172,9 +170,7 @@ export class MetadataSearchDto extends RandomSearchDto {
@Optional()
encodedVideoPath?: string;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder, default: AssetOrder.DESC })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.DESC })
order?: AssetOrder;
@IsInt()
@@ -250,9 +246,7 @@ export enum SearchSuggestionType {
}
export class SearchSuggestionRequestDto {
@IsEnum(SearchSuggestionType)
@IsNotEmpty()
@ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType })
@ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType' })
type!: SearchSuggestionType;
@IsString()
+4 -5
View File
@@ -1,11 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsString } from 'class-validator';
import { IsString } from 'class-validator';
import _ from 'lodash';
import { SharedLink } from 'src/database';
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { SharedLinkType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
export class SharedLinkSearchDto {
@ValidateUUID({ optional: true })
@@ -13,8 +13,7 @@ export class SharedLinkSearchDto {
}
export class SharedLinkCreateDto {
@IsEnum(SharedLinkType)
@ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' })
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' })
type!: SharedLinkType;
@ValidateUUID({ each: true, optional: true })
@@ -90,7 +89,7 @@ export class SharedLinkResponseDto {
userId!: string;
key!: string;
@ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType })
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' })
type!: SharedLinkType;
createdAt!: Date;
expiresAt!: Date | null;
+10 -13
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMaxSize, IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import {
AlbumUserRole,
@@ -13,7 +13,7 @@ import {
UserMetadataKey,
} from 'src/enum';
import { UserMetadata } from 'src/types';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
export class AssetFullSyncDto {
@ValidateUUID({ optional: true })
@@ -90,11 +90,11 @@ export class SyncAssetV1 {
fileModifiedAt!: Date | null;
localDateTime!: Date | null;
duration!: string | null;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' })
type!: AssetType;
deletedAt!: Date | null;
isFavorite!: boolean;
@ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' })
visibility!: AssetVisibility;
livePhotoVideoId!: string | null;
stackId!: string | null;
@@ -159,7 +159,7 @@ export class SyncAlbumUserDeleteV1 {
export class SyncAlbumUserV1 {
albumId!: string;
userId!: string;
@ApiProperty({ enumName: 'AlbumUserRole', enum: AlbumUserRole })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
role!: AlbumUserRole;
}
@@ -173,7 +173,7 @@ export class SyncAlbumV1 {
updatedAt!: Date;
thumbnailAssetId!: string | null;
isActivityEnabled!: boolean;
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' })
order!: AssetOrder;
}
@@ -196,7 +196,7 @@ export class SyncMemoryV1 {
updatedAt!: Date;
deletedAt!: Date | null;
ownerId!: string;
@ApiProperty({ enumName: 'MemoryType', enum: MemoryType })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
type!: MemoryType;
data!: object;
isSaved!: boolean;
@@ -319,8 +319,7 @@ export type SyncItem = {
};
export class SyncStreamDto {
@IsEnum(SyncRequestType, { each: true })
@ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true })
@ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true })
types!: SyncRequestType[];
@ValidateBoolean({ optional: true })
@@ -328,7 +327,7 @@ export class SyncStreamDto {
}
export class SyncAckDto {
@ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType })
@ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType' })
type!: SyncEntityType;
ack!: string;
}
@@ -340,8 +339,6 @@ export class SyncAckSetDto {
}
export class SyncAckDeleteDto {
@IsEnum(SyncEntityType, { each: true })
@ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType, isArray: true })
@Optional()
@ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', optional: true, each: true })
types?: SyncEntityType[];
}
+21 -40
View File
@@ -2,8 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
import { Exclude, Transform, Type } from 'class-transformer';
import {
ArrayMinSize,
IsBoolean,
IsEnum,
IsInt,
IsNotEmpty,
IsNumber,
@@ -34,7 +32,7 @@ import {
VideoContainer,
} from 'src/enum';
import { ConcurrentQueueName } from 'src/types';
import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean } from 'src/validation';
import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
@@ -82,24 +80,19 @@ export class SystemConfigFFmpegDto {
@IsString()
preset!: string;
@IsEnum(VideoCodec)
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec })
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec' })
targetVideoCodec!: VideoCodec;
@IsEnum(VideoCodec, { each: true })
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true })
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true })
acceptedVideoCodecs!: VideoCodec[];
@IsEnum(AudioCodec)
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec })
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec' })
targetAudioCodec!: AudioCodec;
@IsEnum(AudioCodec, { each: true })
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true })
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true })
acceptedAudioCodecs!: AudioCodec[];
@IsEnum(VideoContainer, { each: true })
@ApiProperty({ enumName: 'VideoContainer', enum: VideoContainer, isArray: true })
@ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true })
acceptedContainers!: VideoContainer[];
@IsString()
@@ -131,8 +124,7 @@ export class SystemConfigFFmpegDto {
@ValidateBoolean()
temporalAQ!: boolean;
@IsEnum(CQMode)
@ApiProperty({ enumName: 'CQMode', enum: CQMode })
@ValidateEnum({ enum: CQMode, name: 'CQMode' })
cqMode!: CQMode;
@ValidateBoolean()
@@ -141,19 +133,16 @@ export class SystemConfigFFmpegDto {
@IsString()
preferredHwDevice!: string;
@IsEnum(TranscodePolicy)
@ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy })
@ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' })
transcode!: TranscodePolicy;
@IsEnum(TranscodeHWAccel)
@ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
@ValidateEnum({ enum: TranscodeHWAccel, name: 'TranscodeHWAccel' })
accel!: TranscodeHWAccel;
@ValidateBoolean()
accelDecode!: boolean;
@IsEnum(ToneMapping)
@ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
@ValidateEnum({ enum: ToneMapping, name: 'ToneMapping' })
tonemap!: ToneMapping;
}
@@ -264,8 +253,7 @@ class SystemConfigLoggingDto {
@ValidateBoolean()
enabled!: boolean;
@ApiProperty({ enum: LogLevel, enumName: 'LogLevel' })
@IsEnum(LogLevel)
@ValidateEnum({ enum: LogLevel, name: 'LogLevel' })
level!: LogLevel;
}
@@ -306,8 +294,7 @@ enum MapTheme {
}
export class MapThemeDto {
@IsEnum(MapTheme)
@ApiProperty({ enum: MapTheme, enumName: 'MapTheme' })
@ValidateEnum({ enum: MapTheme, name: 'MapTheme' })
theme!: MapTheme;
}
@@ -368,8 +355,7 @@ class SystemConfigOAuthDto {
@IsString()
clientSecret!: string;
@IsEnum(OAuthTokenEndpointAuthMethod)
@ApiProperty({ enum: OAuthTokenEndpointAuthMethod, enumName: 'OAuthTokenEndpointAuthMethod' })
@ValidateEnum({ enum: OAuthTokenEndpointAuthMethod, name: 'OAuthTokenEndpointAuthMethod' })
tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod;
@IsInt()
@@ -431,7 +417,7 @@ class SystemConfigReverseGeocodingDto {
}
class SystemConfigFacesDto {
@IsBoolean()
@ValidateBoolean()
import!: boolean;
}
@@ -450,12 +436,12 @@ class SystemConfigServerDto {
@IsString()
loginPageMessage!: string;
@IsBoolean()
@ValidateBoolean()
publicUsers!: boolean;
}
class SystemConfigSmtpTransportDto {
@IsBoolean()
@ValidateBoolean()
ignoreCert!: boolean;
@IsNotEmpty()
@@ -475,7 +461,7 @@ class SystemConfigSmtpTransportDto {
}
export class SystemConfigSmtpDto {
@IsBoolean()
@ValidateBoolean()
enabled!: boolean;
@ValidateIf(isEmailNotificationEnabled)
@@ -548,8 +534,7 @@ export class SystemConfigThemeDto {
}
class SystemConfigGeneratedImageDto {
@IsEnum(ImageFormat)
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' })
format!: ImageFormat;
@IsInt()
@@ -567,13 +552,10 @@ class SystemConfigGeneratedImageDto {
}
class SystemConfigGeneratedFullsizeImageDto {
@IsBoolean()
@Type(() => Boolean)
@ApiProperty({ type: 'boolean' })
@ValidateBoolean()
enabled!: boolean;
@IsEnum(ImageFormat)
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' })
format!: ImageFormat;
@IsInt()
@@ -600,8 +582,7 @@ export class SystemConfigImageDto {
@IsObject()
fullsize!: SystemConfigGeneratedFullsizeImageDto;
@IsEnum(Colorspace)
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
@ValidateEnum({ enum: Colorspace, name: 'Colorspace' })
colorspace!: Colorspace;
@ValidateBoolean()
+3 -2
View File
@@ -1,11 +1,12 @@
import { IsBoolean } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
export class AdminOnboardingUpdateDto {
@IsBoolean()
@ValidateBoolean()
isOnboarded!: boolean;
}
export class AdminOnboardingResponseDto {
@ValidateBoolean()
isOnboarded!: boolean;
}
+11 -10
View File
@@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsString } from 'class-validator';
import { IsString } from 'class-validator';
import { AssetOrder, AssetVisibility } from 'src/enum';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
export class TimeBucketDto {
@ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' })
@@ -38,16 +38,17 @@ export class TimeBucketDto {
@ValidateBoolean({ optional: true, description: 'Include assets shared by partners' })
withPartners?: boolean;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({
@ValidateEnum({
enum: AssetOrder,
enumName: 'AssetOrder',
name: 'AssetOrder',
description: 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)',
optional: true,
})
order?: AssetOrder;
@ValidateAssetVisibility({
@ValidateEnum({
enum: AssetVisibility,
name: 'AssetVisibility',
optional: true,
description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)',
})
@@ -93,10 +94,10 @@ export class TimeBucketAssetResponseDto {
})
isFavorite!: boolean[];
@ApiProperty({
@ValidateEnum({
enum: AssetVisibility,
enumName: 'AssetVisibility',
isArray: true,
name: 'AssetVisibility',
each: true,
description: 'Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)',
})
visibility!: AssetVisibility[];
+5 -9
View File
@@ -1,14 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator';
import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator';
import { AssetOrder, UserAvatarColor } from 'src/enum';
import { UserPreferences } from 'src/types';
import { Optional, ValidateBoolean } from 'src/validation';
import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
class AvatarUpdate {
@Optional()
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true })
color?: UserAvatarColor;
}
@@ -23,8 +21,7 @@ class RatingsUpdate {
}
class AlbumsUpdate {
@IsEnum(AssetOrder)
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
defaultAssetOrder?: AssetOrder;
}
@@ -159,8 +156,7 @@ export class UserPreferencesUpdateDto {
}
class AlbumsResponse {
@IsEnum(AssetOrder)
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' })
defaultAssetOrder: AssetOrder = AssetOrder.DESC;
}
+10 -19
View File
@@ -1,10 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
import { IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
import { User, UserAdmin } from 'src/database';
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types';
import { Optional, PinCode, ValidateBoolean, ValidateUUID, toEmail, toSanitized } from 'src/validation';
import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation';
export class UserUpdateMeDto {
@Optional()
@@ -23,9 +23,7 @@ export class UserUpdateMeDto {
@IsNotEmpty()
name?: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
avatarColor?: UserAvatarColor | null;
}
@@ -34,7 +32,7 @@ export class UserResponseDto {
name!: string;
email!: string;
profileImagePath!: string;
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor' })
avatarColor!: UserAvatarColor;
profileChangedAt!: Date;
}
@@ -84,9 +82,7 @@ export class UserAdminCreateDto {
@IsString()
name!: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
avatarColor?: UserAvatarColor | null;
@Optional({ nullable: true })
@@ -103,12 +99,10 @@ export class UserAdminCreateDto {
@ValidateBoolean({ optional: true })
shouldChangePassword?: boolean;
@Optional()
@IsBoolean()
@ValidateBoolean({ optional: true })
notify?: boolean;
@Optional()
@IsBoolean()
@ValidateBoolean({ optional: true })
isAdmin?: boolean;
}
@@ -131,9 +125,7 @@ export class UserAdminUpdateDto {
@IsNotEmpty()
name?: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
avatarColor?: UserAvatarColor | null;
@Optional({ nullable: true })
@@ -150,8 +142,7 @@ export class UserAdminUpdateDto {
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
@Optional()
@IsBoolean()
@ValidateBoolean({ optional: true })
isAdmin?: boolean;
}
@@ -172,7 +163,7 @@ export class UserAdminResponseDto extends UserResponseDto {
quotaSizeInBytes!: number | null;
@ApiProperty({ type: 'integer', format: 'int64' })
quotaUsageInBytes!: number | null;
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
@ValidateEnum({ enum: UserStatus, name: 'UserStatus' })
status!: string;
license!: UserLicense | null;
}
+28 -28
View File
@@ -35,59 +35,59 @@ type Item<T extends EmitEvent> = {
type EventMap = {
// app events
'app.bootstrap': [];
'app.shutdown': [];
AppBootstrap: [];
AppShutdown: [];
'config.init': [{ newConfig: SystemConfig }];
ConfigInit: [{ newConfig: SystemConfig }];
// config events
'config.update': [
ConfigUpdate: [
{
newConfig: SystemConfig;
oldConfig: SystemConfig;
},
];
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
ConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
// album events
'album.update': [{ id: string; recipientId: string }];
'album.invite': [{ id: string; userId: string }];
AlbumUpdate: [{ id: string; recipientId: string }];
AlbumInvite: [{ id: string; userId: string }];
// asset events
'asset.tag': [{ assetId: string }];
'asset.untag': [{ assetId: string }];
'asset.hide': [{ assetId: string; userId: string }];
'asset.show': [{ assetId: string; userId: string }];
'asset.trash': [{ assetId: string; userId: string }];
'asset.delete': [{ assetId: string; userId: string }];
'asset.metadataExtracted': [{ assetId: string; userId: string; source?: JobSource }];
AssetTag: [{ assetId: string }];
AssetUntag: [{ assetId: string }];
AssetHide: [{ assetId: string; userId: string }];
AssetShow: [{ assetId: string; userId: string }];
AssetTrash: [{ assetId: string; userId: string }];
AssetDelete: [{ assetId: string; userId: string }];
AssetMetadataExtracted: [{ assetId: string; userId: string; source?: JobSource }];
// asset bulk events
'assets.trash': [{ assetIds: string[]; userId: string }];
'assets.delete': [{ assetIds: string[]; userId: string }];
'assets.restore': [{ assetIds: string[]; userId: string }];
AssetTrashAll: [{ assetIds: string[]; userId: string }];
AssetDeleteAll: [{ assetIds: string[]; userId: string }];
AssetRestoreAll: [{ assetIds: string[]; userId: string }];
'job.start': [QueueName, JobItem];
'job.failed': [{ job: JobItem; error: Error | any }];
JobStart: [QueueName, JobItem];
JobFailed: [{ job: JobItem; error: Error | any }];
// session events
'session.delete': [{ sessionId: string }];
SessionDelete: [{ sessionId: string }];
// stack events
'stack.create': [{ stackId: string; userId: string }];
'stack.update': [{ stackId: string; userId: string }];
'stack.delete': [{ stackId: string; userId: string }];
StackCreate: [{ stackId: string; userId: string }];
StackUpdate: [{ stackId: string; userId: string }];
StackDelete: [{ stackId: string; userId: string }];
// stack bulk events
'stacks.delete': [{ stackIds: string[]; userId: string }];
StackDeleteAll: [{ stackIds: string[]; userId: string }];
// user events
'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
UserSignup: [{ notify: boolean; id: string; tempPassword?: string }];
// websocket events
'websocket.connect': [{ userId: string }];
WebsocketConnect: [{ userId: string }];
};
export const serverEvents = ['config.update'] as const;
export const serverEvents = ['ConfigUpdate'] as const;
export type ServerEvents = (typeof serverEvents)[number];
export type EmitEvent = keyof EventMap;
@@ -213,7 +213,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
if (auth.session) {
await client.join(auth.session.id);
}
await this.onEvent({ name: 'websocket.connect', args: [{ userId: auth.user.id }], server: false });
await this.onEvent({ name: 'WebsocketConnect', args: [{ userId: auth.user.id }], server: false });
} catch (error: Error | any) {
this.logger.error(`Websocket connection error: ${error}`, error?.stack);
client.emit('error', 'unauthorized');
+1 -1
View File
@@ -89,7 +89,7 @@ export class JobRepository {
this.logger.debug(`Starting worker for queue: ${queueName}`);
this.workers[queueName] = new Worker(
queueName,
(job) => this.eventRepository.emit('job.start', queueName, job as JobItem),
(job) => this.eventRepository.emit('JobStart', queueName, job as JobItem),
{ ...bull.config, concurrency: 1 },
);
}
+17 -1
View File
@@ -265,6 +265,9 @@ export class MapRepository {
throw new Error(`Geodata file ${cities500} not found`);
}
this.logger.log(`Starting geodata import`);
const startTime = performance.now();
const input = createReadStream(cities500, { highWaterMark: 512 * 1024 * 1024 });
let bufferGeodata = [];
const lineReader = readLine.createInterface({ input });
@@ -314,7 +317,20 @@ export class MapRepository {
}
}
await this.db.insertInto('geodata_places').values(bufferGeodata).execute();
if (bufferGeodata.length > 0) {
await this.db.insertInto('geodata_places').values(bufferGeodata).execute();
count += bufferGeodata.length;
}
await Promise.all(futures);
const duration = performance.now() - startTime;
const seconds = duration / 1000;
const recordsPerSecond = Math.round(count / seconds);
this.logger.log(
`Successfully imported ${count} geodata records in ${seconds.toFixed(2)}s (${recordsPerSecond} records/second)`,
);
}
private async loadAdmin(filePath: string) {
+4 -4
View File
@@ -166,7 +166,7 @@ describe(AlbumService.name, () => {
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
expect(mocks.user.getMetadata).toHaveBeenCalledWith(authStub.admin.user.id);
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false);
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
id: albumStub.empty.id,
userId: 'user-id',
});
@@ -209,7 +209,7 @@ describe(AlbumService.name, () => {
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
expect(mocks.user.getMetadata).toHaveBeenCalledWith(authStub.admin.user.id);
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false);
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
id: albumStub.empty.id,
userId: 'user-id',
});
@@ -413,7 +413,7 @@ describe(AlbumService.name, () => {
usersId: authStub.user2.user.id,
albumsId: albumStub.sharedWithAdmin.id,
});
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
id: albumStub.sharedWithAdmin.id,
userId: userStub.user2.id,
});
@@ -662,7 +662,7 @@ describe(AlbumService.name, () => {
albumThumbnailAssetId: 'asset-1',
});
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
expect(mocks.event.emit).toHaveBeenCalledWith('album.update', {
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
id: 'album-123',
recipientId: 'admin_id',
});
+3 -3
View File
@@ -130,7 +130,7 @@ export class AlbumService extends BaseService {
);
for (const { userId } of albumUsers) {
await this.eventRepository.emit('album.invite', { id: album.id, userId });
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId });
}
return mapAlbumWithAssets(album);
@@ -187,7 +187,7 @@ export class AlbumService extends BaseService {
);
for (const recipientId of allUsersExceptUs) {
await this.eventRepository.emit('album.update', { id, recipientId });
await this.eventRepository.emit('AlbumUpdate', { id, recipientId });
}
}
@@ -233,7 +233,7 @@ export class AlbumService extends BaseService {
}
await this.albumUserRepository.create({ usersId: userId, albumsId: id, role });
await this.eventRepository.emit('album.invite', { id, userId });
await this.eventRepository.emit('AlbumInvite', { id, userId });
}
return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
+1 -1
View File
@@ -180,7 +180,7 @@ export class AssetMediaService extends BaseService {
const copiedPhoto = await this.createCopy(asset);
// and immediate trash it
await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.TRASHED });
await this.eventRepository.emit('asset.trash', { assetId: copiedPhoto.id, userId: auth.user.id });
await this.eventRepository.emit('AssetTrash', { assetId: copiedPhoto.id, userId: auth.user.id });
await this.userRepository.updateUsage(auth.user.id, file.size);
+6 -6
View File
@@ -255,7 +255,7 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
@@ -279,7 +279,7 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
@@ -303,7 +303,7 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
@@ -327,7 +327,7 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.HIDDEN,
});
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
@@ -360,7 +360,7 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoMotionAsset.id,
visibility: assetStub.livePhotoStillAsset.visibility,
});
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
@@ -484,7 +484,7 @@ describe(AssetService.name, () => {
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
expect(mocks.event.emit).toHaveBeenCalledWith('assets.delete', {
expect(mocks.event.emit).toHaveBeenCalledWith('AssetDeleteAll', {
assetIds: ['asset1', 'asset2'],
userId: 'user-id',
});
+5 -2
View File
@@ -208,7 +208,7 @@ export class AssetService extends BaseService {
await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0));
}
await this.eventRepository.emit('asset.delete', { assetId: id, userId: asset.ownerId });
await this.eventRepository.emit('AssetDelete', { assetId: id, userId: asset.ownerId });
// delete the motion if it is not used by another asset
if (asset.livePhotoVideoId) {
@@ -241,7 +241,10 @@ export class AssetService extends BaseService {
deletedAt: new Date(),
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
});
await this.eventRepository.emit(force ? 'assets.delete' : 'assets.trash', { assetIds: ids, userId: auth.user.id });
await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', {
assetIds: ids,
userId: auth.user.id,
});
}
async run(auth: AuthDto, dto: AssetJobsDto) {
+1 -1
View File
@@ -179,7 +179,7 @@ describe(AuthService.name, () => {
});
expect(mocks.session.delete).toHaveBeenCalledWith('token123');
expect(mocks.event.emit).toHaveBeenCalledWith('session.delete', { sessionId: 'token123' });
expect(mocks.event.emit).toHaveBeenCalledWith('SessionDelete', { sessionId: 'token123' });
});
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
+1 -1
View File
@@ -80,7 +80,7 @@ export class AuthService extends BaseService {
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
if (auth.session) {
await this.sessionRepository.delete(auth.session.id);
await this.eventRepository.emit('session.delete', { sessionId: auth.session.id });
await this.eventRepository.emit('SessionDelete', { sessionId: auth.session.id });
}
return {
+4 -4
View File
@@ -14,12 +14,12 @@ import { handlePromiseError } from 'src/utils/misc';
export class BackupService extends BaseService {
private backupLock = false;
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
async onConfigInit({
newConfig: {
backup: { database },
},
}: ArgOf<'config.init'>) {
}: ArgOf<'ConfigInit'>) {
this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase);
if (this.backupLock) {
@@ -32,8 +32,8 @@ export class BackupService extends BaseService {
}
}
@OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig: { backup } }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate', server: true })
onConfigUpdate({ newConfig: { backup } }: ArgOf<'ConfigUpdate'>) {
if (!this.backupLock) {
return;
}
+1 -1
View File
@@ -57,7 +57,7 @@ const messages = {
@Injectable()
export class DatabaseService extends BaseService {
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.DatabaseService })
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.DatabaseService })
async onBootstrap() {
const version = await this.databaseRepository.getPostgresVersion();
const current = semver.coerce(version);
+8 -8
View File
@@ -67,8 +67,8 @@ export class JobService extends BaseService {
private services: ClassConstructor<unknown>[] = [];
private nightlyJobsLock = false;
@OnEvent({ name: 'config.init' })
async onConfigInit({ newConfig: config }: ArgOf<'config.init'>) {
@OnEvent({ name: 'ConfigInit' })
async onConfigInit({ newConfig: config }: ArgOf<'ConfigInit'>) {
if (this.worker === ImmichWorker.MICROSERVICES) {
this.updateQueueConcurrency(config);
return;
@@ -87,8 +87,8 @@ export class JobService extends BaseService {
}
}
@OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate', server: true })
onConfigUpdate({ newConfig: config }: ArgOf<'ConfigUpdate'>) {
if (this.worker === ImmichWorker.MICROSERVICES) {
this.updateQueueConcurrency(config);
return;
@@ -101,7 +101,7 @@ export class JobService extends BaseService {
}
}
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService })
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.JobService })
onBootstrap() {
this.jobRepository.setup(this.services);
if (this.worker === ImmichWorker.MICROSERVICES) {
@@ -243,8 +243,8 @@ export class JobService extends BaseService {
}
}
@OnEvent({ name: 'job.start' })
async onJobStart(...[queueName, job]: ArgsOf<'job.start'>) {
@OnEvent({ name: 'JobStart' })
async onJobStart(...[queueName, job]: ArgsOf<'JobStart'>) {
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
try {
@@ -255,7 +255,7 @@ export class JobService extends BaseService {
await this.onDone(job);
}
} catch (error: Error | any) {
await this.eventRepository.emit('job.failed', { job, error });
await this.eventRepository.emit('JobFailed', { job, error });
} finally {
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
}
+5 -5
View File
@@ -32,12 +32,12 @@ export class LibraryService extends BaseService {
private lock = false;
private watchers: Record<string, () => Promise<void>> = {};
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
async onConfigInit({
newConfig: {
library: { watch, scan },
},
}: ArgOf<'config.init'>) {
}: ArgOf<'ConfigInit'>) {
// This ensures that library watching only occurs in one microservice
this.lock = await this.databaseRepository.tryLock(DatabaseLock.Library);
@@ -58,8 +58,8 @@ export class LibraryService extends BaseService {
}
}
@OnEvent({ name: 'config.update', server: true })
async onConfigUpdate({ newConfig: { library } }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate', server: true })
async onConfigUpdate({ newConfig: { library } }: ArgOf<'ConfigUpdate'>) {
if (!this.lock) {
return;
}
@@ -155,7 +155,7 @@ export class LibraryService extends BaseService {
}
}
@OnEvent({ name: 'app.shutdown' })
@OnEvent({ name: 'AppShutdown' })
async onShutdown() {
await this.unwatchAll();
}
+1 -1
View File
@@ -51,7 +51,7 @@ interface UpsertFileOptions {
export class MediaService extends BaseService {
videoInterfaces: VideoInterfaces = { dri: [], mali: false };
@OnEvent({ name: 'app.bootstrap' })
@OnEvent({ name: 'AppBootstrap' })
async onBootstrap() {
const [dri, mali] = await Promise.all([this.getDevices(), this.hasMaliOpenCL()]);
this.videoInterfaces = { dri, mali };
+2 -2
View File
@@ -1368,7 +1368,7 @@ describe(MetadataService.name, () => {
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
userId: assetStub.livePhotoMotionAsset.ownerId,
assetId: assetStub.livePhotoMotionAsset.id,
});
@@ -1384,7 +1384,7 @@ describe(MetadataService.name, () => {
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
expect(mocks.event.emit).toHaveBeenCalledWith('asset.metadataExtracted', {
expect(mocks.event.emit).toHaveBeenCalledWith('AssetMetadataExtracted', {
assetId: assetStub.livePhotoStillAsset.id,
userId: assetStub.livePhotoStillAsset.ownerId,
});
+12 -12
View File
@@ -126,24 +126,24 @@ type Dates = {
@Injectable()
export class MetadataService extends BaseService {
@OnEvent({ name: 'app.bootstrap', workers: [ImmichWorker.MICROSERVICES] })
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.MICROSERVICES] })
async onBootstrap() {
this.logger.log('Bootstrapping metadata service');
await this.init();
}
@OnEvent({ name: 'app.shutdown' })
@OnEvent({ name: 'AppShutdown' })
async onShutdown() {
await this.metadataRepository.teardown();
}
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
onConfigInit({ newConfig }: ArgOf<'config.init'>) {
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) {
this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency);
}
@OnEvent({ name: 'config.update', workers: [ImmichWorker.MICROSERVICES], server: true })
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.MICROSERVICES], server: true })
onConfigUpdate({ newConfig }: ArgOf<'ConfigUpdate'>) {
this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency);
}
@@ -190,7 +190,7 @@ export class MetadataService extends BaseService {
this.albumRepository.removeAssetsFromAll([motionAsset.id]),
]);
await this.eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
await this.eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
}
@OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
@@ -313,7 +313,7 @@ export class MetadataService extends BaseService {
await this.assetRepository.upsertJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() });
await this.eventRepository.emit('asset.metadataExtracted', {
await this.eventRepository.emit('AssetMetadataExtracted', {
assetId: asset.id,
userId: asset.ownerId,
source: data.source,
@@ -351,13 +351,13 @@ export class MetadataService extends BaseService {
return this.processSidecar(id, false);
}
@OnEvent({ name: 'asset.tag' })
async handleTagAsset({ assetId }: ArgOf<'asset.tag'>) {
@OnEvent({ name: 'AssetTag' })
async handleTagAsset({ assetId }: ArgOf<'AssetTag'>) {
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
}
@OnEvent({ name: 'asset.untag' })
async handleUntagAsset({ assetId }: ArgOf<'asset.untag'>) {
@OnEvent({ name: 'AssetUntag' })
async handleUntagAsset({ assetId }: ArgOf<'AssetUntag'>) {
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
}
@@ -64,7 +64,7 @@ describe(NotificationService.name, () => {
const update = { oldConfig: defaults, newConfig: defaults };
expect(sut.onConfigUpdate(update)).toBeUndefined();
expect(mocks.event.clientBroadcast).toHaveBeenCalledWith('on_config_update');
expect(mocks.event.serverSend).toHaveBeenCalledWith('config.update', update);
expect(mocks.event.serverSend).toHaveBeenCalledWith('ConfigUpdate', update);
});
});
+37 -37
View File
@@ -77,8 +77,8 @@ export class NotificationService extends BaseService {
await this.notificationRepository.cleanup();
}
@OnEvent({ name: 'job.failed' })
async onJobFailed({ job, error }: ArgOf<'job.failed'>) {
@OnEvent({ name: 'JobFailed' })
async onJobFailed({ job, error }: ArgOf<'JobFailed'>) {
const admin = await this.userRepository.getAdmin();
if (!admin) {
return;
@@ -107,14 +107,14 @@ export class NotificationService extends BaseService {
}
}
@OnEvent({ name: 'config.update' })
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate' })
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'ConfigUpdate'>) {
this.eventRepository.clientBroadcast('on_config_update');
this.eventRepository.serverSend('config.update', { oldConfig, newConfig });
this.eventRepository.serverSend('ConfigUpdate', { oldConfig, newConfig });
}
@OnEvent({ name: 'config.validate', priority: -100 })
async onConfigValidate({ oldConfig, newConfig }: ArgOf<'config.validate'>) {
@OnEvent({ name: 'ConfigValidate', priority: -100 })
async onConfigValidate({ oldConfig, newConfig }: ArgOf<'ConfigValidate'>) {
try {
if (
newConfig.notifications.smtp.enabled &&
@@ -128,33 +128,33 @@ export class NotificationService extends BaseService {
}
}
@OnEvent({ name: 'asset.hide' })
onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) {
@OnEvent({ name: 'AssetHide' })
onAssetHide({ assetId, userId }: ArgOf<'AssetHide'>) {
this.eventRepository.clientSend('on_asset_hidden', userId, assetId);
}
@OnEvent({ name: 'asset.show' })
async onAssetShow({ assetId }: ArgOf<'asset.show'>) {
@OnEvent({ name: 'AssetShow' })
async onAssetShow({ assetId }: ArgOf<'AssetShow'>) {
await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: { id: assetId, notify: true } });
}
@OnEvent({ name: 'asset.trash' })
onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) {
@OnEvent({ name: 'AssetTrash' })
onAssetTrash({ assetId, userId }: ArgOf<'AssetTrash'>) {
this.eventRepository.clientSend('on_asset_trash', userId, [assetId]);
}
@OnEvent({ name: 'asset.delete' })
onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) {
@OnEvent({ name: 'AssetDelete' })
onAssetDelete({ assetId, userId }: ArgOf<'AssetDelete'>) {
this.eventRepository.clientSend('on_asset_delete', userId, assetId);
}
@OnEvent({ name: 'assets.trash' })
onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) {
@OnEvent({ name: 'AssetTrashAll' })
onAssetsTrash({ assetIds, userId }: ArgOf<'AssetTrashAll'>) {
this.eventRepository.clientSend('on_asset_trash', userId, assetIds);
}
@OnEvent({ name: 'asset.metadataExtracted' })
async onAssetMetadataExtracted({ assetId, userId, source }: ArgOf<'asset.metadataExtracted'>) {
@OnEvent({ name: 'AssetMetadataExtracted' })
async onAssetMetadataExtracted({ assetId, userId, source }: ArgOf<'AssetMetadataExtracted'>) {
if (source !== 'sidecar-write') {
return;
}
@@ -165,40 +165,40 @@ export class NotificationService extends BaseService {
}
}
@OnEvent({ name: 'assets.restore' })
onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) {
@OnEvent({ name: 'AssetRestoreAll' })
onAssetsRestore({ assetIds, userId }: ArgOf<'AssetRestoreAll'>) {
this.eventRepository.clientSend('on_asset_restore', userId, assetIds);
}
@OnEvent({ name: 'stack.create' })
onStackCreate({ userId }: ArgOf<'stack.create'>) {
@OnEvent({ name: 'StackCreate' })
onStackCreate({ userId }: ArgOf<'StackCreate'>) {
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'stack.update' })
onStackUpdate({ userId }: ArgOf<'stack.update'>) {
@OnEvent({ name: 'StackUpdate' })
onStackUpdate({ userId }: ArgOf<'StackUpdate'>) {
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'stack.delete' })
onStackDelete({ userId }: ArgOf<'stack.delete'>) {
@OnEvent({ name: 'StackDelete' })
onStackDelete({ userId }: ArgOf<'StackDelete'>) {
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'stacks.delete' })
onStacksDelete({ userId }: ArgOf<'stacks.delete'>) {
@OnEvent({ name: 'StackDeleteAll' })
onStacksDelete({ userId }: ArgOf<'StackDeleteAll'>) {
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'user.signup' })
async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) {
@OnEvent({ name: 'UserSignup' })
async onUserSignup({ notify, id, tempPassword }: ArgOf<'UserSignup'>) {
if (notify) {
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } });
}
}
@OnEvent({ name: 'album.update' })
async onAlbumUpdate({ id, recipientId }: ArgOf<'album.update'>) {
@OnEvent({ name: 'AlbumUpdate' })
async onAlbumUpdate({ id, recipientId }: ArgOf<'AlbumUpdate'>) {
await this.jobRepository.removeJob(JobName.NOTIFY_ALBUM_UPDATE, `${id}/${recipientId}`);
await this.jobRepository.queue({
name: JobName.NOTIFY_ALBUM_UPDATE,
@@ -206,13 +206,13 @@ export class NotificationService extends BaseService {
});
}
@OnEvent({ name: 'album.invite' })
async onAlbumInvite({ id, userId }: ArgOf<'album.invite'>) {
@OnEvent({ name: 'AlbumInvite' })
async onAlbumInvite({ id, userId }: ArgOf<'AlbumInvite'>) {
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
}
@OnEvent({ name: 'session.delete' })
onSessionDelete({ sessionId }: ArgOf<'session.delete'>) {
@OnEvent({ name: 'SessionDelete' })
onSessionDelete({ sessionId }: ArgOf<'SessionDelete'>) {
// after the response is sent
setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500);
}
+1 -1
View File
@@ -23,7 +23,7 @@ import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchE
@Injectable()
export class ServerService extends BaseService {
@OnEvent({ name: 'app.bootstrap' })
@OnEvent({ name: 'AppBootstrap' })
async onBootstrap(): Promise<void> {
const featureFlags = await this.getFeatures();
if (featureFlags.configFile) {
+6 -6
View File
@@ -10,18 +10,18 @@ import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class SmartInfoService extends BaseService {
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
async onConfigInit({ newConfig }: ArgOf<'config.init'>) {
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
async onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) {
await this.init(newConfig);
}
@OnEvent({ name: 'config.update', workers: [ImmichWorker.MICROSERVICES], server: true })
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.MICROSERVICES], server: true })
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'ConfigUpdate'>) {
await this.init(newConfig, oldConfig);
}
@OnEvent({ name: 'config.validate' })
onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
@OnEvent({ name: 'ConfigValidate' })
onConfigValidate({ newConfig }: ArgOf<'ConfigValidate'>) {
try {
getCLIPModelInfo(newConfig.machineLearning.clip.modelName);
} catch {
+4 -4
View File
@@ -52,7 +52,7 @@ describe(StackService.name, () => {
],
});
expect(mocks.event.emit).toHaveBeenCalledWith('stack.create', {
expect(mocks.event.emit).toHaveBeenCalledWith('StackCreate', {
stackId: 'stack-id',
userId: authStub.admin.user.id,
});
@@ -138,7 +138,7 @@ describe(StackService.name, () => {
id: 'stack-id',
primaryAssetId: assetStub.image1.id,
});
expect(mocks.event.emit).toHaveBeenCalledWith('stack.update', {
expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', {
stackId: 'stack-id',
userId: authStub.admin.user.id,
});
@@ -160,7 +160,7 @@ describe(StackService.name, () => {
await sut.delete(authStub.admin, 'stack-id');
expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id');
expect(mocks.event.emit).toHaveBeenCalledWith('stack.delete', {
expect(mocks.event.emit).toHaveBeenCalledWith('StackDelete', {
stackId: 'stack-id',
userId: authStub.admin.user.id,
});
@@ -182,7 +182,7 @@ describe(StackService.name, () => {
await sut.deleteAll(authStub.admin, { ids: ['stack-id'] });
expect(mocks.stack.deleteAll).toHaveBeenCalledWith(['stack-id']);
expect(mocks.event.emit).toHaveBeenCalledWith('stacks.delete', {
expect(mocks.event.emit).toHaveBeenCalledWith('StackDeleteAll', {
stackIds: ['stack-id'],
userId: authStub.admin.user.id,
});
+4 -4
View File
@@ -21,7 +21,7 @@ export class StackService extends BaseService {
const stack = await this.stackRepository.create({ ownerId: auth.user.id }, dto.assetIds);
await this.eventRepository.emit('stack.create', { stackId: stack.id, userId: auth.user.id });
await this.eventRepository.emit('StackCreate', { stackId: stack.id, userId: auth.user.id });
return mapStack(stack, { auth });
}
@@ -41,7 +41,7 @@ export class StackService extends BaseService {
const updatedStack = await this.stackRepository.update(id, { id, primaryAssetId: dto.primaryAssetId });
await this.eventRepository.emit('stack.update', { stackId: id, userId: auth.user.id });
await this.eventRepository.emit('StackUpdate', { stackId: id, userId: auth.user.id });
return mapStack(updatedStack, { auth });
}
@@ -49,13 +49,13 @@ export class StackService extends BaseService {
async delete(auth: AuthDto, id: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: [id] });
await this.stackRepository.delete(id);
await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
await this.eventRepository.emit('StackDelete', { stackId: id, userId: auth.user.id });
}
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: dto.ids });
await this.stackRepository.deleteAll(dto.ids);
await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id });
await this.eventRepository.emit('StackDeleteAll', { stackIds: dto.ids, userId: auth.user.id });
}
private async findOrFail(id: string) {
+10 -10
View File
@@ -75,8 +75,8 @@ export class StorageTemplateService extends BaseService {
return this._template;
}
@OnEvent({ name: 'config.init' })
onConfigInit({ newConfig }: ArgOf<'config.init'>) {
@OnEvent({ name: 'ConfigInit' })
onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) {
const template = newConfig.storageTemplate.template;
if (!this._template || template !== this.template.raw) {
this.logger.debug(`Compiling new storage template: ${template}`);
@@ -84,13 +84,13 @@ export class StorageTemplateService extends BaseService {
}
}
@OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate', server: true })
onConfigUpdate({ newConfig }: ArgOf<'ConfigUpdate'>) {
this.onConfigInit({ newConfig });
}
@OnEvent({ name: 'config.validate' })
onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
@OnEvent({ name: 'ConfigValidate' })
onConfigValidate({ newConfig }: ArgOf<'ConfigValidate'>) {
try {
const { compiled } = this.compile(newConfig.storageTemplate.template);
this.render(compiled, {
@@ -116,8 +116,8 @@ export class StorageTemplateService extends BaseService {
return { ...storageTokens, presetOptions: storagePresets };
}
@OnEvent({ name: 'asset.metadataExtracted' })
async onAssetMetadataExtracted({ source, assetId }: ArgOf<'asset.metadataExtracted'>) {
@OnEvent({ name: 'AssetMetadataExtracted' })
async onAssetMetadataExtracted({ source, assetId }: ArgOf<'AssetMetadataExtracted'>) {
await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { source, id: assetId } });
}
@@ -182,8 +182,8 @@ export class StorageTemplateService extends BaseService {
return JobStatus.SUCCESS;
}
@OnEvent({ name: 'asset.delete' })
async handleMoveHistoryCleanup({ assetId }: ArgOf<'asset.delete'>) {
@OnEvent({ name: 'AssetDelete' })
async handleMoveHistoryCleanup({ assetId }: ArgOf<'AssetDelete'>) {
this.logger.debug(`Cleaning up move history for asset ${assetId}`);
await this.moveRepository.cleanMoveHistorySingle(assetId);
}
+1 -1
View File
@@ -11,7 +11,7 @@ const docsMessage = `Please see https://immich.app/docs/administration/system-in
@Injectable()
export class StorageService extends BaseService {
@OnEvent({ name: 'app.bootstrap' })
@OnEvent({ name: 'AppBootstrap' })
async onBootstrap() {
const envData = this.configRepository.getEnv();
@@ -412,7 +412,7 @@ describe(SystemConfigService.name, () => {
mocks.systemMetadata.get.mockResolvedValue(partialConfig);
await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig);
expect(mocks.event.emit).toHaveBeenCalledWith(
'config.update',
'ConfigUpdate',
expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
);
});
+10 -10
View File
@@ -12,10 +12,10 @@ import { toPlainObject } from 'src/utils/object';
@Injectable()
export class SystemConfigService extends BaseService {
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.SystemConfig })
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.SystemConfig })
async onBootstrap() {
const config = await this.getConfig({ withCache: false });
await this.eventRepository.emit('config.init', { newConfig: config });
await this.eventRepository.emit('ConfigInit', { newConfig: config });
}
async getSystemConfig(): Promise<SystemConfigDto> {
@@ -27,8 +27,8 @@ export class SystemConfigService extends BaseService {
return mapConfig(defaults);
}
@OnEvent({ name: 'config.init', priority: -100 })
onConfigInit({ newConfig: { logging } }: ArgOf<'config.init'>) {
@OnEvent({ name: 'ConfigInit', priority: -100 })
onConfigInit({ newConfig: { logging } }: ArgOf<'ConfigInit'>) {
const { logLevel: envLevel } = this.configRepository.getEnv();
const configLevel = logging.enabled ? logging.level : false;
const level = envLevel ?? configLevel;
@@ -36,14 +36,14 @@ export class SystemConfigService extends BaseService {
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
}
@OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
@OnEvent({ name: 'ConfigUpdate', server: true })
onConfigUpdate({ newConfig }: ArgOf<'ConfigUpdate'>) {
this.onConfigInit({ newConfig });
clearConfigCache();
}
@OnEvent({ name: 'config.validate' })
onConfigValidate({ newConfig, oldConfig }: ArgOf<'config.validate'>) {
@OnEvent({ name: 'ConfigValidate' })
onConfigValidate({ newConfig, oldConfig }: ArgOf<'ConfigValidate'>) {
const { logLevel } = this.configRepository.getEnv();
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && logLevel) {
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
@@ -59,7 +59,7 @@ export class SystemConfigService extends BaseService {
const oldConfig = await this.getConfig({ withCache: false });
try {
await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig });
await this.eventRepository.emit('ConfigValidate', { newConfig: toPlainObject(dto), oldConfig });
} catch (error) {
this.logger.warn(`Unable to save system config due to a validation error: ${error}`);
throw new BadRequestException(error instanceof Error ? error.message : error);
@@ -67,7 +67,7 @@ export class SystemConfigService extends BaseService {
const newConfig = await this.updateConfig(dto);
await this.eventRepository.emit('config.update', { newConfig, oldConfig });
await this.eventRepository.emit('ConfigUpdate', { newConfig, oldConfig });
return mapConfig(newConfig);
}
+3 -3
View File
@@ -90,7 +90,7 @@ export class TagService extends BaseService {
const results = await this.tagRepository.upsertAssetIds(items);
for (const assetId of new Set(results.map((item) => item.assetsId))) {
await this.eventRepository.emit('asset.tag', { assetId });
await this.eventRepository.emit('AssetTag', { assetId });
}
return { count: results.length };
@@ -107,7 +107,7 @@ export class TagService extends BaseService {
for (const { id: assetId, success } of results) {
if (success) {
await this.eventRepository.emit('asset.tag', { assetId });
await this.eventRepository.emit('AssetTag', { assetId });
}
}
@@ -125,7 +125,7 @@ export class TagService extends BaseService {
for (const { id: assetId, success } of results) {
if (success) {
await this.eventRepository.emit('asset.untag', { assetId });
await this.eventRepository.emit('AssetUntag', { assetId });
}
}
+2 -2
View File
@@ -17,7 +17,7 @@ export class TrashService extends BaseService {
await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
await this.trashRepository.restoreAll(ids);
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
await this.eventRepository.emit('AssetRestoreAll', { assetIds: ids, userId: auth.user.id });
this.logger.log(`Restored ${ids.length} asset(s) from trash`);
@@ -40,7 +40,7 @@ export class TrashService extends BaseService {
return { count };
}
@OnEvent({ name: 'assets.delete' })
@OnEvent({ name: 'AssetDeleteAll' })
async onAssetsDelete() {
await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
}
+1 -1
View File
@@ -35,7 +35,7 @@ export class UserAdminService extends BaseService {
const user = await this.createUser(userDto);
await this.eventRepository.emit('user.signup', {
await this.eventRepository.emit('UserSignup', {
notify: !!notify,
id: user.id,
tempPassword: user.shouldChangePassword ? userDto.password : undefined,
+3 -3
View File
@@ -20,7 +20,7 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
@Injectable()
export class VersionService extends BaseService {
@OnEvent({ name: 'app.bootstrap' })
@OnEvent({ name: 'AppBootstrap' })
async onBootstrap(): Promise<void> {
await this.handleVersionCheck();
@@ -102,8 +102,8 @@ export class VersionService extends BaseService {
return JobStatus.SUCCESS;
}
@OnEvent({ name: 'websocket.connect' })
async onWebsocketConnection({ userId }: ArgOf<'websocket.connect'>) {
@OnEvent({ name: 'WebsocketConnect' })
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
this.eventRepository.clientSend('on_server_version', userId, serverVersion);
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE);
if (metadata) {
+2 -2
View File
@@ -152,7 +152,7 @@ export const onBeforeLink = async (
if (motionAsset && motionAsset.visibility === AssetVisibility.TIMELINE) {
await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.HIDDEN });
await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId });
}
};
@@ -177,7 +177,7 @@ export const onAfterUnlink = async (
{ userId, livePhotoVideoId, visibility }: { userId: string; livePhotoVideoId: string; visibility: AssetVisibility },
) => {
await assetRepository.update({ id: livePhotoVideoId, visibility });
await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId });
await eventRepository.emit('AssetShow', { assetId: livePhotoVideoId, userId });
};
export function mapToUploadFile(file: ImmichFile): UploadFile {

Some files were not shown because too many files have changed in this diff Show More