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

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