Compare commits
18 Commits
rknn-toolk
...
fix/asset-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75d1d21cc6 | ||
|
|
9a4495eb5b | ||
|
|
8ad95b368b | ||
|
|
b778a86c99 | ||
|
|
a65ce2ac55 | ||
|
|
f69d7e7bad | ||
|
|
858d1e9d9b | ||
|
|
a1a61f19eb | ||
|
|
996ffed5eb | ||
|
|
2d7a94ce23 | ||
|
|
72a7be26c0 | ||
|
|
77fad86b82 | ||
|
|
52d90a8280 | ||
|
|
d1c8fe5303 | ||
|
|
a75718ce99 | ||
|
|
d72d715f6b | ||
|
|
16fd19994b | ||
|
|
83ed03920e |
7
.github/workflows/docker.yml
vendored
7
.github/workflows/docker.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
suffix: ["", "-cuda", "-openvino", "-armnn","-rknn"]
|
||||
suffix: ["", "-cuda", "-openvino", "-armnn"]
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -129,9 +129,6 @@ jobs:
|
||||
runner: ubuntu-24.04-arm
|
||||
device: armnn
|
||||
suffix: -armnn
|
||||
- platforms: linux/arm64
|
||||
device: rknn
|
||||
suffix: -rknn
|
||||
|
||||
steps:
|
||||
- name: Prepare
|
||||
@@ -457,4 +454,4 @@ jobs:
|
||||
run: exit 1
|
||||
- name: All jobs passed or skipped
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||
|
||||
222
cli/package-lock.json
generated
222
cli/package-lock.json
generated
@@ -1518,17 +1518,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz",
|
||||
"integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz",
|
||||
"integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.25.0",
|
||||
"@typescript-eslint/type-utils": "8.25.0",
|
||||
"@typescript-eslint/utils": "8.25.0",
|
||||
"@typescript-eslint/visitor-keys": "8.25.0",
|
||||
"@typescript-eslint/scope-manager": "8.26.0",
|
||||
"@typescript-eslint/type-utils": "8.26.0",
|
||||
"@typescript-eslint/utils": "8.26.0",
|
||||
"@typescript-eslint/visitor-keys": "8.26.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1544,20 +1544,20 @@
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.8.0"
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz",
|
||||
"integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz",
|
||||
"integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.25.0",
|
||||
"@typescript-eslint/types": "8.25.0",
|
||||
"@typescript-eslint/typescript-estree": "8.25.0",
|
||||
"@typescript-eslint/visitor-keys": "8.25.0",
|
||||
"@typescript-eslint/scope-manager": "8.26.0",
|
||||
"@typescript-eslint/types": "8.26.0",
|
||||
"@typescript-eslint/typescript-estree": "8.26.0",
|
||||
"@typescript-eslint/visitor-keys": "8.26.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1569,18 +1569,18 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.8.0"
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz",
|
||||
"integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz",
|
||||
"integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.25.0",
|
||||
"@typescript-eslint/visitor-keys": "8.25.0"
|
||||
"@typescript-eslint/types": "8.26.0",
|
||||
"@typescript-eslint/visitor-keys": "8.26.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1591,14 +1591,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz",
|
||||
"integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz",
|
||||
"integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.25.0",
|
||||
"@typescript-eslint/utils": "8.25.0",
|
||||
"@typescript-eslint/typescript-estree": "8.26.0",
|
||||
"@typescript-eslint/utils": "8.26.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.0.1"
|
||||
},
|
||||
@@ -1611,13 +1611,13 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.8.0"
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz",
|
||||
"integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz",
|
||||
"integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1629,14 +1629,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz",
|
||||
"integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz",
|
||||
"integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.25.0",
|
||||
"@typescript-eslint/visitor-keys": "8.25.0",
|
||||
"@typescript-eslint/types": "8.26.0",
|
||||
"@typescript-eslint/visitor-keys": "8.26.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1652,20 +1652,20 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.8.0"
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz",
|
||||
"integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz",
|
||||
"integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.25.0",
|
||||
"@typescript-eslint/types": "8.25.0",
|
||||
"@typescript-eslint/typescript-estree": "8.25.0"
|
||||
"@typescript-eslint/scope-manager": "8.26.0",
|
||||
"@typescript-eslint/types": "8.26.0",
|
||||
"@typescript-eslint/typescript-estree": "8.26.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1676,17 +1676,17 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.8.0"
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz",
|
||||
"integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==",
|
||||
"version": "8.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz",
|
||||
"integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.25.0",
|
||||
"@typescript-eslint/types": "8.26.0",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1711,9 +1711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz",
|
||||
"integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.8.tgz",
|
||||
"integrity": "sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1734,8 +1734,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.0.7",
|
||||
"vitest": "3.0.7"
|
||||
"@vitest/browser": "3.0.8",
|
||||
"vitest": "3.0.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -1744,14 +1744,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz",
|
||||
"integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.8.tgz",
|
||||
"integrity": "sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.7",
|
||||
"@vitest/utils": "3.0.7",
|
||||
"@vitest/spy": "3.0.8",
|
||||
"@vitest/utils": "3.0.8",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -1760,13 +1760,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz",
|
||||
"integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.8.tgz",
|
||||
"integrity": "sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.7",
|
||||
"@vitest/spy": "3.0.8",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
@@ -1787,9 +1787,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz",
|
||||
"integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.8.tgz",
|
||||
"integrity": "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1800,13 +1800,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz",
|
||||
"integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.8.tgz",
|
||||
"integrity": "sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.0.7",
|
||||
"@vitest/utils": "3.0.8",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1814,13 +1814,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz",
|
||||
"integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.8.tgz",
|
||||
"integrity": "sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.7",
|
||||
"@vitest/pretty-format": "3.0.8",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -1829,9 +1829,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz",
|
||||
"integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.8.tgz",
|
||||
"integrity": "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1842,13 +1842,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz",
|
||||
"integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.8.tgz",
|
||||
"integrity": "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.7",
|
||||
"@vitest/pretty-format": "3.0.8",
|
||||
"loupe": "^3.1.3",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -2429,13 +2429,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz",
|
||||
"integrity": "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==",
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
|
||||
"integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "build/bin/cli.js"
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
@@ -3617,9 +3617,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
|
||||
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -4278,9 +4278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -4349,9 +4349,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
|
||||
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
|
||||
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4421,9 +4421,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz",
|
||||
"integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.8.tgz",
|
||||
"integrity": "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4464,19 +4464,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz",
|
||||
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.8.tgz",
|
||||
"integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.0.7",
|
||||
"@vitest/mocker": "3.0.7",
|
||||
"@vitest/pretty-format": "^3.0.7",
|
||||
"@vitest/runner": "3.0.7",
|
||||
"@vitest/snapshot": "3.0.7",
|
||||
"@vitest/spy": "3.0.7",
|
||||
"@vitest/utils": "3.0.7",
|
||||
"@vitest/expect": "3.0.8",
|
||||
"@vitest/mocker": "3.0.8",
|
||||
"@vitest/pretty-format": "^3.0.8",
|
||||
"@vitest/runner": "3.0.8",
|
||||
"@vitest/snapshot": "3.0.8",
|
||||
"@vitest/spy": "3.0.8",
|
||||
"@vitest/utils": "3.0.8",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.0",
|
||||
"expect-type": "^1.1.0",
|
||||
@@ -4488,7 +4488,7 @@
|
||||
"tinypool": "^1.0.2",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vite-node": "3.0.7",
|
||||
"vite-node": "3.0.8",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4504,8 +4504,8 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.0.7",
|
||||
"@vitest/ui": "3.0.7",
|
||||
"@vitest/browser": "3.0.8",
|
||||
"@vitest/ui": "3.0.8",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -4534,9 +4534,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest-fetch-mock": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.4.tgz",
|
||||
"integrity": "sha512-i2RNEAKBgnLWwj5DVz8ouzaHaPVg1xaYgAUmU5p+baJ149upnO+yJLPchAiY9ij8hf0PDkJVVke1pftBxmT05g==",
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.5.tgz",
|
||||
"integrity": "sha512-nhWdCQIGtaSEUVl96pMm0WggyDGPDv5FUy/Q9Hx3cs2RGmh3Q/uRsLClGbdG3kXBkJ3br5yTUjB2MeW25TwdOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -95,12 +95,12 @@ services:
|
||||
image: immich-machine-learning-dev:latest
|
||||
# extends:
|
||||
# file: hwaccel.ml.yml
|
||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
|
||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
||||
build:
|
||||
context: ../machine-learning
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
|
||||
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
||||
ports:
|
||||
- 3003:3003
|
||||
volumes:
|
||||
|
||||
@@ -38,12 +38,12 @@ services:
|
||||
image: immich-machine-learning:latest
|
||||
# extends:
|
||||
# file: hwaccel.ml.yml
|
||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
|
||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
||||
build:
|
||||
context: ../machine-learning
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
|
||||
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
||||
ports:
|
||||
- 3003:3003
|
||||
volumes:
|
||||
@@ -77,12 +77,22 @@ services:
|
||||
- 5432:5432
|
||||
healthcheck:
|
||||
test: >-
|
||||
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
|
||||
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
|
||||
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
|
||||
echo "checksum failure count is $$Chksum";
|
||||
[ "$$Chksum" = '0' ] || exit 1
|
||||
interval: 5m
|
||||
start_interval: 30s
|
||||
start_period: 5m
|
||||
command: >-
|
||||
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
|
||||
postgres
|
||||
-c shared_preload_libraries=vectors.so
|
||||
-c 'search_path="$$user", public, vectors'
|
||||
-c logging_collector=on
|
||||
-c max_wal_size=2GB
|
||||
-c shared_buffers=512MB
|
||||
-c wal_compression=on
|
||||
restart: always
|
||||
|
||||
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
||||
@@ -99,7 +109,7 @@ services:
|
||||
# add data source for http://immich-prometheus:9090 to get started
|
||||
immich-grafana:
|
||||
container_name: immich_grafana
|
||||
command: [ './run.sh', '-disable-reporting' ]
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.5.2-ubuntu@sha256:8b5858c447e06fd7a89006b562ba7bba7c4d5813600c7982374c41852adefaeb
|
||||
|
||||
@@ -33,12 +33,12 @@ services:
|
||||
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
# For hardware acceleration, add one of -[armnn, cuda, openvino, rknn] to the image tag.
|
||||
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
||||
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
|
||||
# file: hwaccel.ml.yml
|
||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||
volumes:
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
@@ -67,12 +67,22 @@ services:
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: >-
|
||||
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
|
||||
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
|
||||
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
|
||||
echo "checksum failure count is $$Chksum";
|
||||
[ "$$Chksum" = '0' ] || exit 1
|
||||
interval: 5m
|
||||
start_interval: 30s
|
||||
start_period: 5m
|
||||
command: >-
|
||||
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
|
||||
postgres
|
||||
-c shared_preload_libraries=vectors.so
|
||||
-c 'search_path="$$user", public, vectors'
|
||||
-c logging_collector=on
|
||||
-c max_wal_size=2GB
|
||||
-c shared_buffers=512MB
|
||||
-c wal_compression=on
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -13,13 +13,6 @@ services:
|
||||
volumes:
|
||||
- /lib/firmware/mali_csffw.bin:/lib/firmware/mali_csffw.bin:ro # Mali firmware for your chipset (not always required depending on the driver)
|
||||
- /usr/lib/libmali.so:/usr/lib/libmali.so:ro # Mali driver for your chipset (always required)
|
||||
|
||||
rknn:
|
||||
security_opt:
|
||||
- systempaths=unconfined
|
||||
- apparmor=unconfined
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
|
||||
cpu: {}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
- ARM NN (Mali)
|
||||
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
|
||||
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
|
||||
- RKNN (Rockchip)
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -20,7 +19,6 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
- Only Linux and Windows (through WSL2) servers are supported.
|
||||
- ARM NN is only supported on devices with Mali GPUs. Other Arm devices are not supported.
|
||||
- Some models may not be compatible with certain backends. CUDA is the most reliable.
|
||||
- Search latency isn't improved by ARM NN due to model compatibility issues preventing its use. However, smart search jobs do make use of ARM NN.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -35,7 +33,6 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
- The `hwaccel.ml.yml` file assumes the path to it is `/usr/lib/libmali.so`, so update accordingly if it is elsewhere
|
||||
- The `hwaccel.ml.yml` file assumes an additional file `/lib/firmware/mali_csffw.bin`, so update accordingly if your device's driver does not require this file
|
||||
- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for ARM NN specific settings
|
||||
- In particular, the `MACHINE_LEARNING_ANN_FP16_TURBO` can significantly improve performance at the cost of very slightly lower accuracy
|
||||
|
||||
#### CUDA
|
||||
|
||||
@@ -50,16 +47,6 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
|
||||
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
|
||||
|
||||
#### RKNN
|
||||
|
||||
- You must have a supported Rockchip SoC: only RK3566, RK3568, RK3576 and RK3588 are supported at this moment.
|
||||
- Make sure you have the appropriate linux kernel driver installed
|
||||
- This is usually pre-installed on the device vendor's Linux images
|
||||
- RKNPU driver V0.9.8 or later must be available in the host server
|
||||
- You may confirm this by running `cat /sys/kernel/debug/rknpu/version` to check the version
|
||||
- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for RKNN specific settings
|
||||
- In particular, setting `MACHINE_LEARNING_RKNN_THREADS` to 2 or 3 can _dramatically_ improve performance for RK3576 and RK3588 compared to the default of 1, at the expense of multiplying the amount of RAM each model uses by that amount.
|
||||
|
||||
## Setup
|
||||
|
||||
1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
||||
@@ -140,12 +127,3 @@ Note that you should increase job concurrencies to increase overall utilization
|
||||
- If you encounter an error when a model is running, try a different model to see if the issue is model-specific.
|
||||
- You may want to increase concurrency past the default for higher utilization. However, keep in mind that this will also increase VRAM consumption.
|
||||
- Larger models benefit more from hardware acceleration, if you have the VRAM for them.
|
||||
- Compared to ARM NN, RKNPU has:
|
||||
- Wider model support (including for search, which ARM NN does not accelerate)
|
||||
- Less heat generation
|
||||
- Very slightly lower accuracy (RKNPU always uses FP16, while ARM NN by default uses higher precision FP32 unless `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled)
|
||||
- Varying speed (tested on RK3588):
|
||||
- If `MACHINE_LEARNING_RKNN_THREADS` is at the default of 1, RKNPU will have substantially lower throughput for ML jobs than ARM NN in most cases, but similar latency (such as when searching)
|
||||
- If `MACHINE_LEARNING_RKNN_THREADS` is set to 3, it will be somewhat faster than ARM NN at FP32, but somewhat slower than ARM NN if `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled
|
||||
- When other tasks also use the GPU (like transcoding), RKNPU has a significant advantage over ARM NN as it uses the otherwise idle NPU instead of competing for GPU usage
|
||||
- Lower RAM usage if `MACHINE_LEARNING_RKNN_THREADS` is at the default of 1, but significantly higher if greater than 1 (which is necessary for it to fully utilize the NPU and hence be comparable in speed to ARM NN)
|
||||
|
||||
@@ -170,8 +170,6 @@ Redis (Sentinel) URL example JSON before encoding:
|
||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
|
||||
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
|
||||
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning |
|
||||
|
||||
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||
|
||||
|
||||
6
docs/package-lock.json
generated
6
docs/package-lock.json
generated
@@ -15734,9 +15734,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
|
||||
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Layout from '@theme/Layout';
|
||||
import { useColorMode } from '@docusaurus/theme-common';
|
||||
import { discordPath, discordViewBox } from '@site/src/components/svg-paths';
|
||||
import ThemedImage from '@theme/ThemedImage';
|
||||
import Icon from '@mdi/react';
|
||||
function HomepageHeader() {
|
||||
const { isDarkTheme } = useColorMode();
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="top-[calc(12%)] md:top-[calc(30%)] h-screen w-full absolute -z-10">
|
||||
@@ -14,8 +12,8 @@ function HomepageHeader() {
|
||||
<div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div>
|
||||
</div>
|
||||
<section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80">
|
||||
<img
|
||||
src={isDarkTheme ? 'img/logomark-dark.svg' : 'img/logomark-light.svg'}
|
||||
<ThemedImage
|
||||
sources={{ dark: 'img/logomark-dark.svg', light: 'img/logomark-light.svg' }}
|
||||
className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
|
||||
alt="Immich logo"
|
||||
/>
|
||||
@@ -35,7 +33,6 @@ function HomepageHeader() {
|
||||
sacrificing your privacy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 ">
|
||||
<Link
|
||||
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-xl no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase"
|
||||
@@ -58,7 +55,6 @@ function HomepageHeader() {
|
||||
Buy Merch
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon
|
||||
path={discordPath}
|
||||
@@ -67,22 +63,19 @@ function HomepageHeader() {
|
||||
/>
|
||||
<Link to="https://discord.immich.app/">Join our Discord</Link>
|
||||
</div>
|
||||
<img
|
||||
src={isDarkTheme ? '/img/screenshot-dark.webp' : '/img/screenshot-light.webp'}
|
||||
<ThemedImage
|
||||
sources={{ dark: '/img/screenshot-dark.webp', light: '/img/screenshot-light.webp' }}
|
||||
alt="screenshots"
|
||||
className="w-[95%] lg:w-[85%] xl:w-[70%] 2xl:w-[60%] "
|
||||
/>
|
||||
|
||||
<div className="mx-[25%] m-auto my-14 md:my-28">
|
||||
<hr className="border bg-gray-500 dark:bg-gray-400" />
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={isDarkTheme ? 'img/logomark-dark.svg' : 'img/logomark-light.svg'}
|
||||
<ThemedImage
|
||||
sources={{ dark: 'img/logomark-dark.svg', light: 'img/logomark-light.svg' }}
|
||||
className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
|
||||
alt="Immich logo"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-bold text-2xl md:text-5xl ">Download the mobile app</p>
|
||||
<p className="text-lg">
|
||||
@@ -101,9 +94,8 @@ function HomepageHeader() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={isDarkTheme ? '/img/app-qr-code-dark.svg' : '/img/app-qr-code-light.svg'}
|
||||
<ThemedImage
|
||||
sources={{ dark: '/img/app-qr-code-dark.svg', light: '/img/app-qr-code-light.svg' }}
|
||||
alt="app qr code"
|
||||
width={'150px'}
|
||||
className="shadow-lg p-3 my-8 dark:bg-immich-dark-bg "
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Layout from '@theme/Layout';
|
||||
import { useColorMode } from '@docusaurus/theme-common';
|
||||
function HomepageHeader() {
|
||||
const { isDarkTheme } = useColorMode();
|
||||
|
||||
return (
|
||||
<header>
|
||||
<section className="max-w-[900px] m-4 p-4 md:p-6 md:m-auto md:my-12 border border-red-400 rounded-2xl bg-slate-200 dark:bg-immich-dark-gray">
|
||||
|
||||
614
e2e/package-lock.json
generated
614
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
machine-learning/.gitignore
vendored
19
machine-learning/.gitignore
vendored
@@ -1,24 +1,5 @@
|
||||
*.zip
|
||||
*.onnx
|
||||
*.rknn
|
||||
*.npy
|
||||
*_attr__value
|
||||
*.weight
|
||||
*.bias
|
||||
onnx__*
|
||||
*in_proj_bias
|
||||
*.proj
|
||||
*.latent
|
||||
*.pos_embed
|
||||
vocab.txt
|
||||
export/immich_model_exporter/models/**/README.md
|
||||
tokenizer.json
|
||||
tokenizer_config.json
|
||||
special_tokens_map.json
|
||||
preprocess_cfg.json
|
||||
config.json
|
||||
merges.txt
|
||||
vocab.json
|
||||
upload/
|
||||
venv/
|
||||
__pycache__/
|
||||
|
||||
@@ -15,13 +15,12 @@ RUN mkdir /opt/armnn && \
|
||||
cd /opt/ann && \
|
||||
sh build.sh
|
||||
|
||||
FROM builder-cpu AS builder-rknn
|
||||
|
||||
FROM builder-${DEVICE} AS builder
|
||||
|
||||
ARG DEVICE
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
PYTHONUNBUFFERED=1 \
|
||||
VIRTUAL_ENV=/opt/venv
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||
@@ -30,7 +29,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest@sha256:562193a4a9d398f8aedddcb223e583da3
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu
|
||||
|
||||
@@ -78,10 +77,6 @@ COPY --from=builder-armnn \
|
||||
/opt/ann/build.sh \
|
||||
/opt/armnn/
|
||||
|
||||
FROM prod-cpu AS prod-rknn
|
||||
|
||||
ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/v2.3.0/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib/
|
||||
|
||||
FROM prod-${DEVICE} AS prod
|
||||
ARG DEVICE
|
||||
|
||||
@@ -95,16 +90,17 @@ WORKDIR /usr/src/app
|
||||
ENV TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/usr/src/app/.venv/bin:$PATH" \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=/usr/src \
|
||||
DEVICE=${DEVICE}
|
||||
DEVICE=${DEVICE} \
|
||||
VIRTUAL_ENV=/opt/venv
|
||||
|
||||
# prevent core dumps
|
||||
RUN echo "hard core 0" >> /etc/security/limits.conf && \
|
||||
echo "fs.suid_dumpable 0" >> /etc/sysctl.conf && \
|
||||
echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile
|
||||
|
||||
COPY --from=builder /usr/src/app/.venv /usr/src/app/.venv
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
COPY ann/ann.py /usr/src/ann/ann.py
|
||||
COPY start.sh log_conf.json gunicorn_conf.py ./
|
||||
COPY app .
|
||||
@@ -127,4 +123,4 @@ ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["./start.sh"]
|
||||
|
||||
HEALTHCHECK CMD python3 healthcheck.py
|
||||
HEALTHCHECK CMD python3 healthcheck.py
|
||||
|
||||
@@ -64,8 +64,6 @@ class Settings(BaseSettings):
|
||||
ann: bool = True
|
||||
ann_fp16_turbo: bool = False
|
||||
ann_tuning_level: int = 2
|
||||
rknn: bool = True
|
||||
rknn_threads: int = 1
|
||||
preload: PreloadModelData | None = None
|
||||
max_batch_size: MaxBatchSize | None = None
|
||||
|
||||
|
||||
@@ -136,12 +136,6 @@ def ann_session() -> Iterator[mock.Mock]:
|
||||
yield mocked
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def rknn_session() -> Iterator[mock.Mock]:
|
||||
with mock.patch("app.sessions.rknn.RknnPoolExecutor") as mocked:
|
||||
yield mocked
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def rmtree() -> Iterator[mock.Mock]:
|
||||
with mock.patch("app.models.base.rmtree", autospec=True) as mocked:
|
||||
|
||||
@@ -226,9 +226,9 @@ async def load(model: InferenceModel) -> InferenceModel:
|
||||
except FileNotFoundError as e:
|
||||
if model.model_format == ModelFormat.ONNX:
|
||||
raise e
|
||||
log.exception(e)
|
||||
log.warning(
|
||||
f"{model.model_format.upper()} is available, but model '{model.model_name}' does not support it.",
|
||||
exc_info=e,
|
||||
f"{model.model_format.upper()} is available, but model '{model.model_name}' does not support it."
|
||||
)
|
||||
model.model_format = ModelFormat.ONNX
|
||||
model.load()
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any, ClassVar
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
import ann.ann
|
||||
import app.sessions.rknn as rknn
|
||||
from app.sessions.ort import OrtSession
|
||||
|
||||
from ..config import clean_name, log, settings
|
||||
@@ -67,17 +66,12 @@ class InferenceModel(ABC):
|
||||
pass
|
||||
|
||||
def _download(self) -> None:
|
||||
ignored_patterns: dict[ModelFormat, list[str]] = {
|
||||
ModelFormat.ONNX: ["*.armnn", "*.rknn"],
|
||||
ModelFormat.ARMNN: ["*.rknn"],
|
||||
ModelFormat.RKNN: ["*.armnn"],
|
||||
}
|
||||
|
||||
ignore_patterns = [] if self.model_format == ModelFormat.ARMNN else ["*.armnn"]
|
||||
snapshot_download(
|
||||
f"immich-app/{clean_name(self.model_name)}",
|
||||
cache_dir=self.cache_dir,
|
||||
local_dir=self.cache_dir,
|
||||
ignore_patterns=ignored_patterns.get(self.model_format, []),
|
||||
ignore_patterns=ignore_patterns,
|
||||
)
|
||||
|
||||
def _load(self) -> ModelSession:
|
||||
@@ -114,25 +108,17 @@ class InferenceModel(ABC):
|
||||
session: ModelSession = AnnSession(model_path)
|
||||
case ".onnx":
|
||||
session = OrtSession(model_path)
|
||||
case ".rknn":
|
||||
session = rknn.RknnSession(model_path)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported model file type: {model_path.suffix}")
|
||||
return session
|
||||
|
||||
def model_path_for_format(self, model_format: ModelFormat) -> Path:
|
||||
model_path_prefix = rknn.model_prefix if model_format == ModelFormat.RKNN else None
|
||||
if model_path_prefix:
|
||||
return self.model_dir / model_path_prefix / f"model.{model_format}"
|
||||
return self.model_dir / f"model.{model_format}"
|
||||
|
||||
@property
|
||||
def model_dir(self) -> Path:
|
||||
return self.cache_dir / self.model_type.value
|
||||
|
||||
@property
|
||||
def model_path(self) -> Path:
|
||||
return self.model_path_for_format(self.model_format)
|
||||
return self.model_dir / f"model.{self.model_format}"
|
||||
|
||||
@property
|
||||
def model_task(self) -> ModelTask:
|
||||
@@ -169,9 +155,4 @@ class InferenceModel(ABC):
|
||||
|
||||
@property
|
||||
def _model_format_default(self) -> ModelFormat:
|
||||
if rknn.is_available:
|
||||
return ModelFormat.RKNN
|
||||
elif ann.ann.is_available and settings.ann:
|
||||
return ModelFormat.ARMNN
|
||||
else:
|
||||
return ModelFormat.ONNX
|
||||
return ModelFormat.ARMNN if ann.ann.is_available and settings.ann else ModelFormat.ONNX
|
||||
|
||||
@@ -44,18 +44,6 @@ _OPENCLIP_MODELS = {
|
||||
"nllb-clip-base-siglip__v1",
|
||||
"nllb-clip-large-siglip__mrl",
|
||||
"nllb-clip-large-siglip__v1",
|
||||
"ViT-B-16-SigLIP2__webli",
|
||||
"ViT-B-32-SigLIP2-256__webli",
|
||||
"ViT-L-16-SigLIP2-256__webli",
|
||||
"ViT-L-16-SigLIP2-384__webli",
|
||||
"ViT-L-16-SigLIP2-512__webli",
|
||||
"ViT-SO400M-14-SigLIP2-378__webli",
|
||||
"ViT-SO400M-14-SigLIP2__webli",
|
||||
"ViT-SO400M-16-SigLIP2-256__webli",
|
||||
"ViT-SO400M-16-SigLIP2-384__webli",
|
||||
"ViT-SO400M-16-SigLIP2-512__webli",
|
||||
"ViT-gopt-16-SigLIP2-256__webli",
|
||||
"ViT-gopt-16-SigLIP2-384__webli",
|
||||
}
|
||||
|
||||
|
||||
@@ -77,9 +65,6 @@ _INSIGHTFACE_MODELS = {
|
||||
|
||||
SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"]
|
||||
|
||||
RKNN_SUPPORTED_SOCS = ["rk3566", "rk3568", "rk3576", "rk3588"]
|
||||
RKNN_COREMASK_SUPPORTED_SOCS = ["rk3576", "rk3588"]
|
||||
|
||||
|
||||
def get_model_source(model_name: str) -> ModelSource | None:
|
||||
cleaned_name = clean_name(model_name)
|
||||
|
||||
@@ -31,7 +31,7 @@ class FaceRecognizer(InferenceModel):
|
||||
self._add_batch_axis(self.model_path)
|
||||
session = self._make_session(self.model_path)
|
||||
self.model = ArcFaceONNX(
|
||||
self.model_path_for_format(ModelFormat.ONNX).as_posix(),
|
||||
self.model_path.with_suffix(".onnx").as_posix(),
|
||||
session=session,
|
||||
)
|
||||
return session
|
||||
|
||||
@@ -35,7 +35,6 @@ class ModelType(StrEnum):
|
||||
class ModelFormat(StrEnum):
|
||||
ARMNN = "armnn"
|
||||
ONNX = "onnx"
|
||||
RKNN = "rknn"
|
||||
|
||||
|
||||
class ModelSource(StrEnum):
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from app.config import log, settings
|
||||
from app.schemas import SessionNode
|
||||
|
||||
from .rknnpool import RknnPoolExecutor, is_available, soc_name
|
||||
|
||||
is_available = is_available and settings.rknn
|
||||
model_prefix = Path("rknpu") / soc_name if is_available and soc_name is not None else None
|
||||
|
||||
|
||||
def run_inference(rknn_lite: Any, input: list[NDArray[np.float32]]) -> list[NDArray[np.float32]]:
|
||||
outputs: list[NDArray[np.float32]] = rknn_lite.inference(inputs=input, data_format="nchw")
|
||||
return outputs
|
||||
|
||||
|
||||
input_output_mapping: dict[str, dict[str, Any]] = {
|
||||
"detection": {
|
||||
"input": {"norm_tensor:0": (1, 3, 640, 640)},
|
||||
"output": {
|
||||
"norm_tensor:1": (12800, 1),
|
||||
"norm_tensor:2": (3200, 1),
|
||||
"norm_tensor:3": (800, 1),
|
||||
"norm_tensor:4": (12800, 4),
|
||||
"norm_tensor:5": (3200, 4),
|
||||
"norm_tensor:6": (800, 4),
|
||||
"norm_tensor:7": (12800, 10),
|
||||
"norm_tensor:8": (3200, 10),
|
||||
"norm_tensor:9": (800, 10),
|
||||
},
|
||||
},
|
||||
"recognition": {"input": {"norm_tensor:0": (1, 3, 112, 112)}, "output": {"norm_tensor:1": (1, 512)}},
|
||||
}
|
||||
|
||||
|
||||
class RknnSession:
|
||||
def __init__(self, model_path: Path) -> None:
|
||||
self.model_type = "detection" if "detection" in model_path.parts else "recognition"
|
||||
self.tpe = settings.rknn_threads
|
||||
|
||||
log.info(f"Loading RKNN model from {model_path} with {self.tpe} threads.")
|
||||
self.rknnpool = RknnPoolExecutor(model_path=model_path.as_posix(), tpes=self.tpe, func=run_inference)
|
||||
log.info(f"Loaded RKNN model from {model_path} with {self.tpe} threads.")
|
||||
|
||||
def get_inputs(self) -> list[SessionNode]:
|
||||
return [RknnNode(name=k, shape=v) for k, v in input_output_mapping[self.model_type]["input"].items()]
|
||||
|
||||
def get_outputs(self) -> list[SessionNode]:
|
||||
return [RknnNode(name=k, shape=v) for k, v in input_output_mapping[self.model_type]["output"].items()]
|
||||
|
||||
def run(
|
||||
self,
|
||||
output_names: list[str] | None,
|
||||
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
|
||||
run_options: Any = None,
|
||||
) -> list[NDArray[np.float32]]:
|
||||
input_data: list[NDArray[np.float32]] = [np.ascontiguousarray(v) for v in input_feed.values()]
|
||||
self.rknnpool.put(input_data)
|
||||
res = self.rknnpool.get()
|
||||
if res is None:
|
||||
raise RuntimeError("RKNN inference failed!")
|
||||
return res
|
||||
|
||||
|
||||
class RknnNode(NamedTuple):
|
||||
name: str | None
|
||||
shape: tuple[int, ...]
|
||||
|
||||
|
||||
__all__ = ["RknnSession", "RknnNode", "is_available", "soc_name", "model_prefix"]
|
||||
@@ -1,91 +0,0 @@
|
||||
# This code is from leafqycc/rknn-multi-threaded
|
||||
# Following Apache License 2.0
|
||||
|
||||
import logging
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from app.config import log
|
||||
from app.models.constants import RKNN_COREMASK_SUPPORTED_SOCS, RKNN_SUPPORTED_SOCS
|
||||
|
||||
|
||||
def get_soc(device_tree_path: Path | str) -> str | None:
|
||||
try:
|
||||
with Path(device_tree_path).open() as f:
|
||||
device_compatible_str = f.read()
|
||||
for soc in RKNN_SUPPORTED_SOCS:
|
||||
if soc in device_compatible_str:
|
||||
return soc
|
||||
log.warning("Device is not supported for RKNN")
|
||||
except OSError as e:
|
||||
log.warning(f"Could not read {device_tree_path}. Reason: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
soc_name = None
|
||||
is_available = False
|
||||
try:
|
||||
from rknnlite.api import RKNNLite
|
||||
|
||||
soc_name = get_soc("/proc/device-tree/compatible")
|
||||
is_available = soc_name is not None
|
||||
except ImportError:
|
||||
log.debug("RKNN is not available")
|
||||
|
||||
|
||||
def init_rknn(model_path: str) -> "RKNNLite":
|
||||
if not is_available:
|
||||
raise RuntimeError("rknn is not available!")
|
||||
rknn_lite = RKNNLite()
|
||||
rknn_lite.rknn_log.logger.setLevel(logging.ERROR)
|
||||
ret = rknn_lite.load_rknn(model_path)
|
||||
if ret != 0:
|
||||
raise RuntimeError("Failed to load RKNN model")
|
||||
|
||||
if soc_name in RKNN_COREMASK_SUPPORTED_SOCS:
|
||||
ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_AUTO)
|
||||
else:
|
||||
ret = rknn_lite.init_runtime() # Please do not set this parameter on other platforms.
|
||||
|
||||
if ret != 0:
|
||||
raise RuntimeError("Failed to inititalize RKNN runtime environment")
|
||||
|
||||
return rknn_lite
|
||||
|
||||
|
||||
class RknnPoolExecutor:
|
||||
def __init__(
|
||||
self,
|
||||
model_path: str,
|
||||
tpes: int,
|
||||
func: Callable[["RKNNLite", list[NDArray[np.float32]]], list[NDArray[np.float32]]],
|
||||
) -> None:
|
||||
self.tpes = tpes
|
||||
self.queue: Queue[Future[list[NDArray[np.float32]]]] = Queue()
|
||||
self.rknn_pool = [init_rknn(model_path) for _ in range(tpes)]
|
||||
self.pool = ThreadPoolExecutor(max_workers=tpes)
|
||||
self.func = func
|
||||
self.num = 0
|
||||
|
||||
def put(self, inputs: list[NDArray[np.float32]]) -> None:
|
||||
self.queue.put(self.pool.submit(self.func, self.rknn_pool[self.num % self.tpes], inputs))
|
||||
self.num += 1
|
||||
|
||||
def get(self) -> list[NDArray[np.float32]] | None:
|
||||
if self.queue.empty():
|
||||
return None
|
||||
fut = self.queue.get()
|
||||
return fut.result()
|
||||
|
||||
def release(self) -> None:
|
||||
self.pool.shutdown()
|
||||
for rknn_lite in self.rknn_pool:
|
||||
rknn_lite.release()
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.release()
|
||||
@@ -25,7 +25,6 @@ from app.models.facial_recognition.detection import FaceDetector
|
||||
from app.models.facial_recognition.recognition import FaceRecognizer
|
||||
from app.sessions.ann import AnnSession
|
||||
from app.sessions.ort import OrtSession
|
||||
from app.sessions.rknn import RknnSession, run_inference
|
||||
|
||||
from .config import Settings, settings
|
||||
from .models.base import InferenceModel
|
||||
@@ -70,14 +69,6 @@ class TestBase:
|
||||
|
||||
assert encoder.model_format == ModelFormat.ARMNN
|
||||
|
||||
def test_sets_default_model_format_to_rknn_if_available(self, mocker: MockerFixture) -> None:
|
||||
mocker.patch.object(settings, "rknn", True)
|
||||
mocker.patch("app.sessions.rknn.is_available", True)
|
||||
|
||||
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
|
||||
|
||||
assert encoder.model_format == ModelFormat.RKNN
|
||||
|
||||
def test_casts_cache_dir_string_to_path(self) -> None:
|
||||
cache_dir = "/test_cache"
|
||||
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=cache_dir)
|
||||
@@ -134,7 +125,7 @@ class TestBase:
|
||||
"immich-app/ViT-B-32__openai",
|
||||
cache_dir=encoder.cache_dir,
|
||||
local_dir=encoder.cache_dir,
|
||||
ignore_patterns=["*.armnn", "*.rknn"],
|
||||
ignore_patterns=["*.armnn"],
|
||||
)
|
||||
|
||||
def test_download_downloads_armnn_if_preferred_format(self, snapshot_download: mock.Mock) -> None:
|
||||
@@ -145,18 +136,7 @@ class TestBase:
|
||||
"immich-app/ViT-B-32__openai",
|
||||
cache_dir=encoder.cache_dir,
|
||||
local_dir=encoder.cache_dir,
|
||||
ignore_patterns=["*.rknn"],
|
||||
)
|
||||
|
||||
def test_download_downloads_rknn_if_preferred_format(self, snapshot_download: mock.Mock) -> None:
|
||||
encoder = OpenClipTextualEncoder("ViT-B-32__openai", model_format=ModelFormat.RKNN)
|
||||
encoder.download()
|
||||
|
||||
snapshot_download.assert_called_once_with(
|
||||
"immich-app/ViT-B-32__openai",
|
||||
cache_dir=encoder.cache_dir,
|
||||
local_dir=encoder.cache_dir,
|
||||
ignore_patterns=["*.armnn"],
|
||||
ignore_patterns=[],
|
||||
)
|
||||
|
||||
def test_throws_exception_if_model_path_does_not_exist(
|
||||
@@ -348,33 +328,6 @@ class TestAnnSession:
|
||||
np_spy.assert_has_calls([mock.call(input1), mock.call(input2)])
|
||||
|
||||
|
||||
class TestRknnSession:
|
||||
def test_creates_rknn_session(self, rknn_session: mock.Mock, info: mock.Mock, mocker: MockerFixture) -> None:
|
||||
model_path = mock.MagicMock(spec=Path)
|
||||
tpe = 1
|
||||
mocker.patch("app.sessions.rknn.soc_name", "rk3566")
|
||||
mocker.patch("app.sessions.rknn.is_available", True)
|
||||
RknnSession(model_path)
|
||||
|
||||
rknn_session.assert_called_once_with(model_path=model_path.as_posix(), tpes=tpe, func=run_inference)
|
||||
|
||||
info.assert_has_calls([mock.call(f"Loaded RKNN model from {model_path} with {tpe} threads.")])
|
||||
|
||||
def test_run_rknn(self, rknn_session: mock.Mock, mocker: MockerFixture) -> None:
|
||||
rknn_session.return_value.load.return_value = 123
|
||||
np_spy = mocker.spy(np, "ascontiguousarray")
|
||||
mocker.patch("app.sessions.rknn.soc_name", "rk3566")
|
||||
session = RknnSession(Path("ViT-B-32__openai"))
|
||||
[input1, input2] = [np.random.rand(1, 3, 224, 224).astype(np.float32) for _ in range(2)]
|
||||
input_feed = {"input.1": input1, "input.2": input2}
|
||||
|
||||
session.run(None, input_feed)
|
||||
|
||||
rknn_session.return_value.put.assert_called_once_with([input1, input2])
|
||||
np_spy.call_count == 2
|
||||
np_spy.assert_has_calls([mock.call(input1), mock.call(input2)])
|
||||
|
||||
|
||||
class TestCLIP:
|
||||
embedding = np.random.rand(512).astype(np.float32)
|
||||
cache_dir = Path("test_cache")
|
||||
@@ -876,7 +829,9 @@ class TestLoad:
|
||||
mock_model.clear_cache.assert_not_called()
|
||||
mock_model.load.assert_not_called()
|
||||
|
||||
async def test_falls_back_to_onnx_if_other_format_does_not_exist(self, warning: mock.Mock) -> None:
|
||||
async def test_falls_back_to_onnx_if_other_format_does_not_exist(
|
||||
self, exception: mock.Mock, warning: mock.Mock
|
||||
) -> None:
|
||||
mock_model = mock.Mock(spec=InferenceModel)
|
||||
mock_model.model_name = "test_model_name"
|
||||
mock_model.model_type = ModelType.VISUAL
|
||||
@@ -891,9 +846,8 @@ class TestLoad:
|
||||
|
||||
mock_model.clear_cache.assert_not_called()
|
||||
assert mock_model.load.call_count == 2
|
||||
warning.assert_called_once_with(
|
||||
"ARMNN is available, but model 'test_model_name' does not support it.", exc_info=error
|
||||
)
|
||||
exception.assert_called_once_with(error)
|
||||
warning.assert_called_once_with("ARMNN is available, but model 'test_model_name' does not support it.")
|
||||
mock_model.model_format = ModelFormat.ONNX
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.12
|
||||
20
machine-learning/export/Dockerfile
Normal file
20
machine-learning/export/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder
|
||||
|
||||
ENV TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=/usr/src
|
||||
|
||||
COPY --chown=$MAMBA_USER:$MAMBA_USER conda-lock.yml /tmp/conda-lock.yml
|
||||
RUN micromamba install -y -n base -f /tmp/conda-lock.yml && \
|
||||
micromamba remove -y -n base cxx-compiler && \
|
||||
micromamba clean --all --yes
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --chown=$MAMBA_USER:$MAMBA_USER start.sh .
|
||||
COPY --chown=$MAMBA_USER:$MAMBA_USER app .
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/_entrypoint.sh"]
|
||||
CMD ["./start.sh"]
|
||||
4328
machine-learning/export/conda-lock.yml
Normal file
4328
machine-learning/export/conda-lock.yml
Normal file
File diff suppressed because it is too large
Load Diff
15
machine-learning/export/env.dev.yaml
Normal file
15
machine-learning/export/env.dev.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
name: base
|
||||
channels:
|
||||
- conda-forge
|
||||
platforms:
|
||||
- linux-64
|
||||
- linux-aarch64
|
||||
dependencies:
|
||||
- black
|
||||
- conda-lock
|
||||
- mypy
|
||||
- pytest
|
||||
- pytest-cov
|
||||
- pytest-mock
|
||||
- ruff
|
||||
category: dev
|
||||
25
machine-learning/export/env.yaml
Normal file
25
machine-learning/export/env.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
name: base
|
||||
channels:
|
||||
- conda-forge
|
||||
- nvidia
|
||||
- pytorch
|
||||
platforms:
|
||||
- linux-64
|
||||
dependencies:
|
||||
- cxx-compiler
|
||||
- onnx==1.*
|
||||
- onnxruntime==1.*
|
||||
- open-clip-torch==2.*
|
||||
- orjson==3.*
|
||||
- pip
|
||||
- python==3.11.*
|
||||
- pytorch>=2.3
|
||||
- rich==13.*
|
||||
- safetensors==0.*
|
||||
- setuptools==68.*
|
||||
- torchvision
|
||||
- transformers==4.*
|
||||
- pip:
|
||||
- multilingual-clip
|
||||
- onnxsim
|
||||
category: main
|
||||
@@ -1,98 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from .exporters.constants import DELETE_PATTERNS, SOURCE_TO_METADATA, ModelSource
|
||||
from .exporters.onnx import export as onnx_export
|
||||
from .exporters.rknn import export as rknn_export
|
||||
|
||||
app = typer.Typer(pretty_exceptions_show_locals=False)
|
||||
|
||||
|
||||
def generate_readme(model_name: str, model_source: ModelSource) -> str:
|
||||
(name, link, type) = SOURCE_TO_METADATA[model_source]
|
||||
match model_source:
|
||||
case ModelSource.MCLIP:
|
||||
tags = ["immich", "clip", "multilingual"]
|
||||
case ModelSource.OPENCLIP:
|
||||
tags = ["immich", "clip"]
|
||||
lowered = model_name.lower()
|
||||
if "xlm" in lowered or "nllb" in lowered:
|
||||
tags.append("multilingual")
|
||||
case ModelSource.INSIGHTFACE:
|
||||
tags = ["immich", "facial-recognition"]
|
||||
case _:
|
||||
raise ValueError(f"Unsupported model source {model_source}")
|
||||
|
||||
return f"""---
|
||||
tags:
|
||||
{" - " + "\n - ".join(tags)}
|
||||
---
|
||||
# Model Description
|
||||
|
||||
This repo contains ONNX exports for the associated {type} model by {name}. See the [{name}]({link}) repo for more info.
|
||||
|
||||
This repo is specifically intended for use with [Immich](https://immich.app/), a self-hosted photo library.
|
||||
"""
|
||||
|
||||
|
||||
@app.command()
|
||||
def main(
|
||||
model_name: str,
|
||||
model_source: ModelSource,
|
||||
output_dir: Path = Path("./models"),
|
||||
no_cache: bool = False,
|
||||
hf_organization: str = "immich-app",
|
||||
hf_auth_token: Annotated[str | None, typer.Option(envvar="HF_AUTH_TOKEN")] = None,
|
||||
) -> None:
|
||||
hf_model_name = model_name.split("/")[-1]
|
||||
hf_model_name = hf_model_name.replace("xlm-roberta-large", "XLM-Roberta-Large")
|
||||
hf_model_name = hf_model_name.replace("xlm-roberta-base", "XLM-Roberta-Base")
|
||||
output_dir = output_dir / hf_model_name
|
||||
match model_source:
|
||||
case ModelSource.MCLIP | ModelSource.OPENCLIP:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
onnx_export(model_name, model_source, output_dir, no_cache=no_cache)
|
||||
case ModelSource.INSIGHTFACE:
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
# TODO: start from insightface dump instead of downloading from HF
|
||||
snapshot_download(f"immich-app/{hf_model_name}", local_dir=output_dir)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported model source {model_source}")
|
||||
|
||||
try:
|
||||
rknn_export(output_dir, no_cache=no_cache)
|
||||
except Exception as e:
|
||||
print(f"Failed to export model {model_name} to rknn: {e}")
|
||||
(output_dir / "rknpu").unlink(missing_ok=True)
|
||||
|
||||
readme_path = output_dir / "README.md"
|
||||
if no_cache or not readme_path.exists():
|
||||
with open(readme_path, "w") as f:
|
||||
f.write(generate_readme(model_name, model_source))
|
||||
|
||||
if hf_auth_token is not None:
|
||||
from huggingface_hub import create_repo, upload_folder
|
||||
|
||||
repo_id = f"{hf_organization}/{hf_model_name}"
|
||||
|
||||
@retry(stop=stop_after_attempt(5), wait=wait_fixed(5))
|
||||
def upload_model() -> None:
|
||||
create_repo(repo_id, exist_ok=True, token=hf_auth_token)
|
||||
upload_folder(
|
||||
repo_id=repo_id,
|
||||
folder_path=output_dir,
|
||||
# remote repo files to be deleted before uploading
|
||||
# deletion is in the same commit as the upload, so it's atomic
|
||||
delete_patterns=DELETE_PATTERNS,
|
||||
token=hf_auth_token,
|
||||
)
|
||||
|
||||
upload_model()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
typer.run(main)
|
||||
@@ -1,42 +0,0 @@
|
||||
from enum import StrEnum
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class ModelSource(StrEnum):
|
||||
INSIGHTFACE = "insightface"
|
||||
MCLIP = "mclip"
|
||||
OPENCLIP = "openclip"
|
||||
|
||||
|
||||
class SourceMetadata(NamedTuple):
|
||||
name: str
|
||||
link: str
|
||||
type: str
|
||||
|
||||
|
||||
SOURCE_TO_METADATA = {
|
||||
ModelSource.MCLIP: SourceMetadata("M-CLIP", "https://huggingface.co/M-CLIP", "CLIP"),
|
||||
ModelSource.OPENCLIP: SourceMetadata("OpenCLIP", "https://github.com/mlfoundations/open_clip", "CLIP"),
|
||||
ModelSource.INSIGHTFACE: SourceMetadata(
|
||||
"InsightFace", "https://github.com/deepinsight/insightface/tree/master", "facial recognition"
|
||||
),
|
||||
}
|
||||
|
||||
RKNN_SOCS = ["rk3566", "rk3568", "rk3576", "rk3588"]
|
||||
|
||||
|
||||
# glob to delete old UUID blobs when reuploading models
|
||||
_uuid_char = "[a-fA-F0-9]"
|
||||
_uuid_glob = _uuid_char * 8 + "-" + _uuid_char * 4 + "-" + _uuid_char * 4 + "-" + _uuid_char * 4 + "-" + _uuid_char * 12
|
||||
DELETE_PATTERNS = [
|
||||
"**/*onnx*",
|
||||
"**/Constant*",
|
||||
"**/*.weight",
|
||||
"**/*.bias",
|
||||
"**/*.proj",
|
||||
"**/*in_proj_bias",
|
||||
"**/*.npy",
|
||||
"**/*.latent",
|
||||
"**/*.pos_embed",
|
||||
f"**/{_uuid_glob}",
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from ..constants import ModelSource
|
||||
from .models import mclip, openclip
|
||||
|
||||
|
||||
def export(
|
||||
model_name: str, model_source: ModelSource, output_dir: Path, opset_version: int = 19, no_cache: bool = False
|
||||
) -> None:
|
||||
visual_dir = output_dir / "visual"
|
||||
textual_dir = output_dir / "textual"
|
||||
match model_source:
|
||||
case ModelSource.MCLIP:
|
||||
mclip.to_onnx(model_name, opset_version, visual_dir, textual_dir, no_cache=no_cache)
|
||||
case ModelSource.OPENCLIP:
|
||||
name, _, pretrained = model_name.partition("__")
|
||||
config = openclip.OpenCLIPModelConfig(name, pretrained)
|
||||
openclip.to_onnx(config, opset_version, visual_dir, textual_dir, no_cache=no_cache)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported model source {model_source}")
|
||||
@@ -1,153 +0,0 @@
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .util import get_model_path, save_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenCLIPModelConfig:
|
||||
name: str
|
||||
pretrained: str
|
||||
|
||||
@cached_property
|
||||
def model_config(self) -> dict[str, Any]:
|
||||
import open_clip
|
||||
|
||||
config: dict[str, Any] | None = open_clip.get_model_config(self.name)
|
||||
if config is None:
|
||||
raise ValueError(f"Unknown model {self.name}")
|
||||
return config
|
||||
|
||||
@property
|
||||
def image_size(self) -> int:
|
||||
image_size: int = self.model_config["vision_cfg"]["image_size"]
|
||||
return image_size
|
||||
|
||||
@property
|
||||
def sequence_length(self) -> int:
|
||||
context_length: int = self.model_config["text_cfg"].get("context_length", 77)
|
||||
return context_length
|
||||
|
||||
|
||||
def to_onnx(
|
||||
model_cfg: OpenCLIPModelConfig,
|
||||
opset_version: int,
|
||||
output_dir_visual: Path | str | None = None,
|
||||
output_dir_textual: Path | str | None = None,
|
||||
no_cache: bool = False,
|
||||
) -> tuple[Path | None, Path | None]:
|
||||
visual_path = None
|
||||
textual_path = None
|
||||
if output_dir_visual is not None:
|
||||
output_dir_visual = Path(output_dir_visual)
|
||||
visual_path = get_model_path(output_dir_visual)
|
||||
|
||||
if output_dir_textual is not None:
|
||||
output_dir_textual = Path(output_dir_textual)
|
||||
textual_path = get_model_path(output_dir_textual)
|
||||
|
||||
if not no_cache and (
|
||||
(textual_path is None or textual_path.exists()) and (visual_path is None or visual_path.exists())
|
||||
):
|
||||
print(f"Models {textual_path} and {visual_path} already exist, skipping")
|
||||
return visual_path, textual_path
|
||||
|
||||
import open_clip
|
||||
import torch
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
torch.backends.mha.set_fastpath_enabled(False)
|
||||
|
||||
model = open_clip.create_model(
|
||||
model_cfg.name,
|
||||
pretrained=model_cfg.pretrained,
|
||||
jit=False,
|
||||
require_pretrained=True,
|
||||
)
|
||||
|
||||
text_vision_cfg = open_clip.get_model_config(model_cfg.name)
|
||||
|
||||
model.eval()
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(False)
|
||||
|
||||
if visual_path is not None and output_dir_visual is not None:
|
||||
if no_cache or not visual_path.exists():
|
||||
save_config(
|
||||
open_clip.get_model_preprocess_cfg(model),
|
||||
output_dir_visual / "preprocess_cfg.json",
|
||||
)
|
||||
save_config(text_vision_cfg, output_dir_visual.parent / "config.json")
|
||||
_export_image_encoder(model, model_cfg, visual_path, opset_version)
|
||||
else:
|
||||
print(f"Model {visual_path} already exists, skipping")
|
||||
|
||||
if textual_path is not None and output_dir_textual is not None:
|
||||
if no_cache or not textual_path.exists():
|
||||
tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32")
|
||||
AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual)
|
||||
_export_text_encoder(model, model_cfg, textual_path, opset_version)
|
||||
else:
|
||||
print(f"Model {textual_path} already exists, skipping")
|
||||
return visual_path, textual_path
|
||||
|
||||
|
||||
def _export_image_encoder(
|
||||
model: Any, model_cfg: OpenCLIPModelConfig, output_path: Path | str, opset_version: int
|
||||
) -> None:
|
||||
import torch
|
||||
|
||||
output_path = Path(output_path)
|
||||
|
||||
def encode_image(image: torch.Tensor) -> torch.Tensor:
|
||||
output = model.encode_image(image, normalize=True)
|
||||
assert isinstance(output, torch.Tensor)
|
||||
return output
|
||||
|
||||
model.forward = encode_image
|
||||
|
||||
args = (torch.randn(1, 3, model_cfg.image_size, model_cfg.image_size),)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
torch.onnx.export(
|
||||
model,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["image"],
|
||||
output_names=["embedding"],
|
||||
opset_version=opset_version,
|
||||
# dynamic_axes={"image": {0: "batch_size"}},
|
||||
)
|
||||
|
||||
|
||||
def _export_text_encoder(
|
||||
model: Any, model_cfg: OpenCLIPModelConfig, output_path: Path | str, opset_version: int
|
||||
) -> None:
|
||||
import torch
|
||||
|
||||
output_path = Path(output_path)
|
||||
|
||||
def encode_text(text: torch.Tensor) -> torch.Tensor:
|
||||
output = model.encode_text(text, normalize=True)
|
||||
assert isinstance(output, torch.Tensor)
|
||||
return output
|
||||
|
||||
model.forward = encode_text
|
||||
|
||||
args = (torch.ones(1, model_cfg.sequence_length, dtype=torch.int32),)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
torch.onnx.export(
|
||||
model,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["text"],
|
||||
output_names=["embedding"],
|
||||
opset_version=opset_version,
|
||||
# dynamic_axes={"text": {0: "batch_size"}},
|
||||
)
|
||||
@@ -1,96 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from .constants import RKNN_SOCS
|
||||
|
||||
|
||||
def _export_platform(
|
||||
model_dir: Path,
|
||||
target_platform: str,
|
||||
inputs: list[str] | None = None,
|
||||
input_size_list: list[list[int]] | None = None,
|
||||
fuse_matmul_softmax_matmul_to_sdpa: bool = True,
|
||||
no_cache: bool = False,
|
||||
) -> None:
|
||||
from rknn.api import RKNN
|
||||
|
||||
input_path = model_dir / "model.onnx"
|
||||
output_path = model_dir / "rknpu" / target_platform / "model.rknn"
|
||||
if not no_cache and output_path.exists():
|
||||
print(f"Model {input_path} already exists at {output_path}, skipping")
|
||||
return
|
||||
|
||||
print(f"Exporting model {input_path} to {output_path}")
|
||||
|
||||
rknn = RKNN(verbose=False)
|
||||
|
||||
rknn.config(
|
||||
target_platform=target_platform,
|
||||
disable_rules=["fuse_matmul_softmax_matmul_to_sdpa"] if not fuse_matmul_softmax_matmul_to_sdpa else [],
|
||||
enable_flash_attention=False,
|
||||
model_pruning=True,
|
||||
)
|
||||
ret = rknn.load_onnx(model=input_path.as_posix(), inputs=inputs, input_size_list=input_size_list)
|
||||
|
||||
if ret != 0:
|
||||
raise RuntimeError("Load failed!")
|
||||
|
||||
ret = rknn.build(do_quantization=False)
|
||||
|
||||
if ret != 0:
|
||||
raise RuntimeError("Build failed!")
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
ret = rknn.export_rknn(output_path.as_posix())
|
||||
if ret != 0:
|
||||
raise RuntimeError("Export rknn model failed!")
|
||||
|
||||
|
||||
def _export_platforms(
|
||||
model_dir: Path,
|
||||
inputs: list[str] | None = None,
|
||||
input_size_list: list[list[int]] | None = None,
|
||||
no_cache: bool = False,
|
||||
) -> None:
|
||||
fuse_matmul_softmax_matmul_to_sdpa = True
|
||||
for soc in RKNN_SOCS:
|
||||
try:
|
||||
_export_platform(
|
||||
model_dir,
|
||||
soc,
|
||||
inputs=inputs,
|
||||
input_size_list=input_size_list,
|
||||
fuse_matmul_softmax_matmul_to_sdpa=fuse_matmul_softmax_matmul_to_sdpa,
|
||||
no_cache=no_cache,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to export model for {soc}: {e}")
|
||||
if "inputs or 'outputs' must be set" in str(e):
|
||||
print("Retrying without fuse_matmul_softmax_matmul_to_sdpa")
|
||||
fuse_matmul_softmax_matmul_to_sdpa = False
|
||||
_export_platform(
|
||||
model_dir,
|
||||
soc,
|
||||
inputs=inputs,
|
||||
input_size_list=input_size_list,
|
||||
fuse_matmul_softmax_matmul_to_sdpa=fuse_matmul_softmax_matmul_to_sdpa,
|
||||
no_cache=no_cache,
|
||||
)
|
||||
|
||||
|
||||
def export(model_dir: Path, no_cache: bool = False) -> None:
|
||||
textual = model_dir / "textual"
|
||||
visual = model_dir / "visual"
|
||||
detection = model_dir / "detection"
|
||||
recognition = model_dir / "recognition"
|
||||
|
||||
if textual.is_dir():
|
||||
_export_platforms(textual, no_cache=no_cache)
|
||||
|
||||
if visual.is_dir():
|
||||
_export_platforms(visual, no_cache=no_cache)
|
||||
|
||||
if detection.is_dir():
|
||||
_export_platforms(detection, inputs=["input.1"], input_size_list=[[1, 3, 640, 640]], no_cache=no_cache)
|
||||
|
||||
if recognition.is_dir():
|
||||
_export_platforms(recognition, inputs=["input.1"], input_size_list=[[1, 3, 112, 112]], no_cache=no_cache)
|
||||
@@ -1,88 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
from exporters.constants import ModelSource
|
||||
|
||||
mclip = [
|
||||
"M-CLIP/LABSE-Vit-L-14",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
|
||||
]
|
||||
|
||||
openclip = [
|
||||
"RN101__openai",
|
||||
"RN101__yfcc15m",
|
||||
"RN50__cc12m",
|
||||
"RN50__openai",
|
||||
"RN50__yfcc15m",
|
||||
"RN50x16__openai",
|
||||
"RN50x4__openai",
|
||||
"RN50x64__openai",
|
||||
"ViT-B-16-SigLIP-256__webli",
|
||||
"ViT-B-16-SigLIP-384__webli",
|
||||
"ViT-B-16-SigLIP-512__webli",
|
||||
"ViT-B-16-SigLIP-i18n-256__webli",
|
||||
"ViT-B-16-SigLIP2__webli",
|
||||
"ViT-B-16-SigLIP__webli",
|
||||
"ViT-B-16-plus-240__laion400m_e31",
|
||||
"ViT-B-16-plus-240__laion400m_e32",
|
||||
"ViT-B-16__laion400m_e31",
|
||||
"ViT-B-16__laion400m_e32",
|
||||
"ViT-B-16__openai",
|
||||
"ViT-B-32-SigLIP2-256__webli",
|
||||
"ViT-B-32__laion2b-s34b-b79k",
|
||||
"ViT-B-32__laion2b_e16",
|
||||
"ViT-B-32__laion400m_e31",
|
||||
"ViT-B-32__laion400m_e32",
|
||||
"ViT-B-32__openai",
|
||||
"ViT-H-14-378-quickgelu__dfn5b",
|
||||
"ViT-H-14-quickgelu__dfn5b",
|
||||
"ViT-H-14__laion2b-s32b-b79k",
|
||||
"ViT-L-14-336__openai",
|
||||
"ViT-L-14-quickgelu__dfn2b",
|
||||
"ViT-L-14__laion2b-s32b-b82k",
|
||||
"ViT-L-14__laion400m_e31",
|
||||
"ViT-L-14__laion400m_e32",
|
||||
"ViT-L-14__openai",
|
||||
"ViT-L-16-SigLIP-256__webli",
|
||||
"ViT-L-16-SigLIP-384__webli",
|
||||
"ViT-L-16-SigLIP2-256__webli",
|
||||
"ViT-L-16-SigLIP2-384__webli",
|
||||
"ViT-L-16-SigLIP2-512__webli",
|
||||
"ViT-SO400M-14-SigLIP-384__webli",
|
||||
"ViT-SO400M-14-SigLIP2-378__webli",
|
||||
"ViT-SO400M-14-SigLIP2__webli",
|
||||
"ViT-SO400M-16-SigLIP2-256__webli",
|
||||
"ViT-SO400M-16-SigLIP2-384__webli",
|
||||
"ViT-SO400M-16-SigLIP2-512__webli",
|
||||
"ViT-gopt-16-SigLIP2-256__webli",
|
||||
"ViT-gopt-16-SigLIP2-384__webli",
|
||||
"nllb-clip-base-siglip__mrl",
|
||||
"nllb-clip-base-siglip__v1",
|
||||
"nllb-clip-large-siglip__mrl",
|
||||
"nllb-clip-large-siglip__v1",
|
||||
"xlm-roberta-base-ViT-B-32__laion5b_s13b_b90k",
|
||||
"xlm-roberta-large-ViT-H-14__frozen_laion5b_s13b_b90k",
|
||||
]
|
||||
|
||||
insightface = [
|
||||
"antelopev2",
|
||||
"buffalo_l",
|
||||
"buffalo_m",
|
||||
"buffalo_s",
|
||||
]
|
||||
|
||||
|
||||
def export_models(models: list[str], source: ModelSource) -> None:
|
||||
for model in models:
|
||||
try:
|
||||
print(f"Exporting model {model}")
|
||||
subprocess.check_call(["python", "-m", "immich_model_exporter.export", model, source])
|
||||
except Exception as e:
|
||||
print(f"Failed to export model {model}: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export_models(mclip, ModelSource.MCLIP)
|
||||
export_models(openclip, ModelSource.OPENCLIP)
|
||||
export_models(insightface, ModelSource.INSIGHTFACE)
|
||||
@@ -1,6 +1,11 @@
|
||||
import os
|
||||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from .openclip import OpenCLIPModelConfig
|
||||
from .openclip import to_onnx as openclip_to_onnx
|
||||
@@ -16,40 +21,25 @@ _MCLIP_TO_OPENCLIP = {
|
||||
|
||||
def to_onnx(
|
||||
model_name: str,
|
||||
opset_version: int,
|
||||
output_dir_visual: Path | str,
|
||||
output_dir_textual: Path | str,
|
||||
no_cache: bool = False,
|
||||
) -> tuple[Path, Path]:
|
||||
textual_path = get_model_path(output_dir_textual)
|
||||
if no_cache or not textual_path.exists():
|
||||
import torch
|
||||
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
torch.backends.mha.set_fastpath_enabled(False)
|
||||
|
||||
model = MultilingualCLIP.from_pretrained(model_name)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model = MultilingualCLIP.from_pretrained(model_name, cache_dir=os.environ.get("CACHE_DIR", tmpdir))
|
||||
AutoTokenizer.from_pretrained(model_name).save_pretrained(output_dir_textual)
|
||||
|
||||
model.eval()
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(False)
|
||||
|
||||
_export_text_encoder(model, textual_path, opset_version)
|
||||
else:
|
||||
print(f"Model {textual_path} already exists, skipping")
|
||||
visual_path, _ = openclip_to_onnx(
|
||||
_MCLIP_TO_OPENCLIP[model_name], opset_version, output_dir_visual, no_cache=no_cache
|
||||
)
|
||||
assert visual_path is not None, "Visual model export failed"
|
||||
export_text_encoder(model, textual_path)
|
||||
visual_path, _ = openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual)
|
||||
assert visual_path is not None, "Visual model export failed"
|
||||
return visual_path, textual_path
|
||||
|
||||
|
||||
def _export_text_encoder(model: Any, output_path: Path | str, opset_version: int) -> None:
|
||||
import torch
|
||||
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
|
||||
|
||||
def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> None:
|
||||
output_path = Path(output_path)
|
||||
|
||||
def forward(self: MultilingualCLIP, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
|
||||
@@ -71,7 +61,7 @@ def _export_text_encoder(model: Any, output_path: Path | str, opset_version: int
|
||||
output_path.as_posix(),
|
||||
input_names=["input_ids", "attention_mask"],
|
||||
output_names=["embedding"],
|
||||
opset_version=opset_version,
|
||||
opset_version=17,
|
||||
# dynamic_axes={
|
||||
# "input_ids": {0: "batch_size", 1: "sequence_length"},
|
||||
# "attention_mask": {0: "batch_size", 1: "sequence_length"},
|
||||
114
machine-learning/export/models/openclip.py
Normal file
114
machine-learning/export/models/openclip.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import tempfile
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import open_clip
|
||||
import torch
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from .util import get_model_path, save_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenCLIPModelConfig:
|
||||
name: str
|
||||
pretrained: str
|
||||
image_size: int = field(init=False)
|
||||
sequence_length: int = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
open_clip_cfg = open_clip.get_model_config(self.name)
|
||||
if open_clip_cfg is None:
|
||||
raise ValueError(f"Unknown model {self.name}")
|
||||
self.image_size = open_clip_cfg["vision_cfg"]["image_size"]
|
||||
self.sequence_length = open_clip_cfg["text_cfg"].get("context_length", 77)
|
||||
|
||||
|
||||
def to_onnx(
|
||||
model_cfg: OpenCLIPModelConfig,
|
||||
output_dir_visual: Path | str | None = None,
|
||||
output_dir_textual: Path | str | None = None,
|
||||
) -> tuple[Path | None, Path | None]:
|
||||
visual_path = None
|
||||
textual_path = None
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model = open_clip.create_model(
|
||||
model_cfg.name,
|
||||
pretrained=model_cfg.pretrained,
|
||||
jit=False,
|
||||
cache_dir=os.environ.get("CACHE_DIR", tmpdir),
|
||||
require_pretrained=True,
|
||||
)
|
||||
|
||||
text_vision_cfg = open_clip.get_model_config(model_cfg.name)
|
||||
|
||||
model.eval()
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(False)
|
||||
|
||||
if output_dir_visual is not None:
|
||||
output_dir_visual = Path(output_dir_visual)
|
||||
visual_path = get_model_path(output_dir_visual)
|
||||
|
||||
save_config(open_clip.get_model_preprocess_cfg(model), output_dir_visual / "preprocess_cfg.json")
|
||||
save_config(text_vision_cfg, output_dir_visual.parent / "config.json")
|
||||
export_image_encoder(model, model_cfg, visual_path)
|
||||
|
||||
if output_dir_textual is not None:
|
||||
output_dir_textual = Path(output_dir_textual)
|
||||
textual_path = get_model_path(output_dir_textual)
|
||||
|
||||
tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32")
|
||||
AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual)
|
||||
export_text_encoder(model, model_cfg, textual_path)
|
||||
return visual_path, textual_path
|
||||
|
||||
|
||||
def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None:
|
||||
output_path = Path(output_path)
|
||||
|
||||
def encode_image(image: torch.Tensor) -> torch.Tensor:
|
||||
output = model.encode_image(image, normalize=True)
|
||||
assert isinstance(output, torch.Tensor)
|
||||
return output
|
||||
|
||||
args = (torch.randn(1, 3, model_cfg.image_size, model_cfg.image_size),)
|
||||
traced = torch.jit.trace(encode_image, args) # type: ignore[no-untyped-call]
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
torch.onnx.export(
|
||||
traced,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["image"],
|
||||
output_names=["embedding"],
|
||||
opset_version=17,
|
||||
# dynamic_axes={"image": {0: "batch_size"}},
|
||||
)
|
||||
|
||||
|
||||
def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None:
|
||||
output_path = Path(output_path)
|
||||
|
||||
def encode_text(text: torch.Tensor) -> torch.Tensor:
|
||||
output = model.encode_text(text, normalize=True)
|
||||
assert isinstance(output, torch.Tensor)
|
||||
return output
|
||||
|
||||
args = (torch.ones(1, model_cfg.sequence_length, dtype=torch.int32),)
|
||||
traced = torch.jit.trace(encode_text, args) # type: ignore[no-untyped-call]
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
torch.onnx.export(
|
||||
traced,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["text"],
|
||||
output_names=["embedding"],
|
||||
opset_version=17,
|
||||
# dynamic_axes={"text": {0: "batch_size"}},
|
||||
)
|
||||
49
machine-learning/export/models/optimize.py
Normal file
49
machine-learning/export/models/optimize.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
|
||||
import onnx
|
||||
import onnxruntime as ort
|
||||
import onnxsim
|
||||
|
||||
|
||||
def save_onnx(model: onnx.ModelProto, output_path: Path | str) -> None:
|
||||
try:
|
||||
onnx.save(model, output_path)
|
||||
except ValueError as e:
|
||||
if "The proto size is larger than the 2 GB limit." in str(e):
|
||||
onnx.save(model, output_path, save_as_external_data=True, size_threshold=1_000_000)
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
def optimize_onnxsim(model_path: Path | str, output_path: Path | str) -> None:
|
||||
model_path = Path(model_path)
|
||||
output_path = Path(output_path)
|
||||
model = onnx.load(model_path.as_posix())
|
||||
model, check = onnxsim.simplify(model)
|
||||
assert check, "Simplified ONNX model could not be validated"
|
||||
for file in model_path.parent.iterdir():
|
||||
if file.name.startswith("Constant") or "onnx" in file.name or file.suffix == ".weight":
|
||||
file.unlink()
|
||||
save_onnx(model, output_path)
|
||||
|
||||
|
||||
def optimize_ort(
|
||||
model_path: Path | str,
|
||||
output_path: Path | str,
|
||||
level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC,
|
||||
) -> None:
|
||||
model_path = Path(model_path)
|
||||
output_path = Path(output_path)
|
||||
|
||||
sess_options = ort.SessionOptions()
|
||||
sess_options.graph_optimization_level = level
|
||||
sess_options.optimized_model_filepath = output_path.as_posix()
|
||||
|
||||
ort.InferenceSession(model_path.as_posix(), providers=["CPUExecutionProvider"], sess_options=sess_options)
|
||||
|
||||
|
||||
def optimize(model_path: Path | str) -> None:
|
||||
model_path = Path(model_path)
|
||||
|
||||
optimize_ort(model_path, model_path)
|
||||
optimize_onnxsim(model_path, model_path)
|
||||
@@ -1,67 +0,0 @@
|
||||
[project]
|
||||
name = "immich_model_exporter"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10, <4.0"
|
||||
dependencies = [
|
||||
"huggingface-hub>=0.29.3",
|
||||
"multilingual-clip>=1.0.10",
|
||||
"onnx>=1.14.1",
|
||||
"onnxruntime>=1.16.0",
|
||||
"open-clip-torch>=2.31.0",
|
||||
"typer>=0.15.2",
|
||||
"rknn-toolkit2>=2.3.0",
|
||||
"transformers>=4.49.0",
|
||||
"tenacity>=9.0.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["black>=23.3.0", "mypy>=1.3.0", "ruff>=0.0.272"]
|
||||
|
||||
[tool.uv]
|
||||
override-dependencies = [
|
||||
"onnx>=1.16.0,<2",
|
||||
"onnxruntime>=1.18.2,<2",
|
||||
"torch>=2.4",
|
||||
"torchvision>=0.21",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
torch = [{ index = "pytorch-cpu" }]
|
||||
torchvision = [{ index = "pytorch-cpu" }]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "pytorch-cpu"
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
explicit = true
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["immich_model_exporter"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["immich_model_exporter"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
follow_imports = "silent"
|
||||
warn_redundant_casts = true
|
||||
disallow_any_generics = true
|
||||
check_untyped_defs = true
|
||||
disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ['py312']
|
||||
113
machine-learning/export/run.py
Normal file
113
machine-learning/export/run.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import gc
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import torch
|
||||
from huggingface_hub import create_repo, upload_folder
|
||||
from models import mclip, openclip
|
||||
from models.optimize import optimize
|
||||
from rich.progress import Progress
|
||||
|
||||
models = [
|
||||
"M-CLIP/LABSE-Vit-L-14",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
|
||||
"RN101::openai",
|
||||
"RN101::yfcc15m",
|
||||
"RN50::cc12m",
|
||||
"RN50::openai",
|
||||
"RN50::yfcc15m",
|
||||
"RN50x16::openai",
|
||||
"RN50x4::openai",
|
||||
"RN50x64::openai",
|
||||
"ViT-B-16-SigLIP-256::webli",
|
||||
"ViT-B-16-SigLIP-384::webli",
|
||||
"ViT-B-16-SigLIP-512::webli",
|
||||
"ViT-B-16-SigLIP-i18n-256::webli",
|
||||
"ViT-B-16-SigLIP::webli",
|
||||
"ViT-B-16-plus-240::laion400m_e31",
|
||||
"ViT-B-16-plus-240::laion400m_e32",
|
||||
"ViT-B-16::laion400m_e31",
|
||||
"ViT-B-16::laion400m_e32",
|
||||
"ViT-B-16::openai",
|
||||
"ViT-B-32::laion2b-s34b-b79k",
|
||||
"ViT-B-32::laion2b_e16",
|
||||
"ViT-B-32::laion400m_e31",
|
||||
"ViT-B-32::laion400m_e32",
|
||||
"ViT-B-32::openai",
|
||||
"ViT-H-14-378-quickgelu::dfn5b",
|
||||
"ViT-H-14-quickgelu::dfn5b",
|
||||
"ViT-H-14::laion2b-s32b-b79k",
|
||||
"ViT-L-14-336::openai",
|
||||
"ViT-L-14-quickgelu::dfn2b",
|
||||
"ViT-L-14::laion2b-s32b-b82k",
|
||||
"ViT-L-14::laion400m_e31",
|
||||
"ViT-L-14::laion400m_e32",
|
||||
"ViT-L-14::openai",
|
||||
"ViT-L-16-SigLIP-256::webli",
|
||||
"ViT-L-16-SigLIP-384::webli",
|
||||
"ViT-SO400M-14-SigLIP-384::webli",
|
||||
"ViT-g-14::laion2b-s12b-b42k",
|
||||
"nllb-clip-base-siglip::mrl",
|
||||
"nllb-clip-base-siglip::v1",
|
||||
"nllb-clip-large-siglip::mrl",
|
||||
"nllb-clip-large-siglip::v1",
|
||||
"xlm-roberta-base-ViT-B-32::laion5b_s13b_b90k",
|
||||
"xlm-roberta-large-ViT-H-14::frozen_laion5b_s13b_b90k",
|
||||
]
|
||||
|
||||
# glob to delete old UUID blobs when reuploading models
|
||||
uuid_char = "[a-fA-F0-9]"
|
||||
uuid_glob = uuid_char * 8 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 12
|
||||
|
||||
# remote repo files to be deleted before uploading
|
||||
# deletion is in the same commit as the upload, so it's atomic
|
||||
delete_patterns = ["**/*onnx*", "**/Constant*", "**/*.weight", "**/*.bias", f"**/{uuid_glob}"]
|
||||
|
||||
with Progress() as progress:
|
||||
task = progress.add_task("[green]Exporting models...", total=len(models))
|
||||
token = os.environ.get("HF_AUTH_TOKEN")
|
||||
torch.backends.mha.set_fastpath_enabled(False)
|
||||
with TemporaryDirectory() as tmp:
|
||||
tmpdir = Path(tmp)
|
||||
for model in models:
|
||||
model_name = model.split("/")[-1].replace("::", "__")
|
||||
hf_model_name = model_name.replace("xlm-roberta-large", "XLM-Roberta-Large")
|
||||
hf_model_name = model_name.replace("xlm-roberta-base", "XLM-Roberta-Base")
|
||||
config_path = tmpdir / model_name / "config.json"
|
||||
|
||||
def export() -> None:
|
||||
progress.update(task, description=f"[green]Exporting {hf_model_name}")
|
||||
visual_dir = tmpdir / hf_model_name / "visual"
|
||||
textual_dir = tmpdir / hf_model_name / "textual"
|
||||
if model.startswith("M-CLIP"):
|
||||
visual_path, textual_path = mclip.to_onnx(model, visual_dir, textual_dir)
|
||||
else:
|
||||
name, _, pretrained = model_name.partition("__")
|
||||
config = openclip.OpenCLIPModelConfig(name, pretrained)
|
||||
visual_path, textual_path = openclip.to_onnx(config, visual_dir, textual_dir)
|
||||
progress.update(task, description=f"[green]Optimizing {hf_model_name} (visual)")
|
||||
optimize(visual_path)
|
||||
progress.update(task, description=f"[green]Optimizing {hf_model_name} (textual)")
|
||||
optimize(textual_path)
|
||||
|
||||
gc.collect()
|
||||
|
||||
def upload() -> None:
|
||||
progress.update(task, description=f"[yellow]Uploading {hf_model_name}")
|
||||
repo_id = f"immich-app/{hf_model_name}"
|
||||
|
||||
create_repo(repo_id, exist_ok=True)
|
||||
upload_folder(
|
||||
repo_id=repo_id,
|
||||
folder_path=tmpdir / hf_model_name,
|
||||
delete_patterns=delete_patterns,
|
||||
token=token,
|
||||
)
|
||||
|
||||
export()
|
||||
if token is not None:
|
||||
upload()
|
||||
progress.update(task, advance=1)
|
||||
1395
machine-learning/export/uv.lock
generated
1395
machine-learning/export/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,6 @@ cpu = ["onnxruntime>=1.15.0,<2"]
|
||||
cuda = ["onnxruntime-gpu>=1.17.0,<2"]
|
||||
openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0"]
|
||||
armnn = ["onnxruntime>=1.15.0,<2"]
|
||||
rknn = ["onnxruntime>=1.15.0,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
|
||||
|
||||
[tool.uv]
|
||||
compile-bytecode = true
|
||||
|
||||
79
machine-learning/uv.lock
generated
79
machine-learning/uv.lock
generated
@@ -1109,10 +1109,6 @@ cuda = [
|
||||
openvino = [
|
||||
{ name = "onnxruntime-openvino" },
|
||||
]
|
||||
rknn = [
|
||||
{ name = "onnxruntime" },
|
||||
{ name = "rknn-toolkit-lite2" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
@@ -1166,7 +1162,6 @@ requires-dist = [
|
||||
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
||||
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
|
||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
|
||||
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
|
||||
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" },
|
||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
|
||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||
@@ -1176,11 +1171,10 @@ requires-dist = [
|
||||
{ name = "pydantic-settings", specifier = ">=2.5.2,<3" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.6,<1.0" },
|
||||
{ name = "rich", specifier = ">=13.4.2" },
|
||||
{ name = "rknn-toolkit-lite2", marker = "extra == 'rknn'", specifier = ">=2.3.0,<3" },
|
||||
{ name = "tokenizers", specifier = ">=0.15.0,<1.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.22.0,<1.0" },
|
||||
]
|
||||
provides-extras = ["cpu", "cuda", "openvino", "armnn", "rknn"]
|
||||
provides-extras = ["cpu", "cuda", "openvino", "armnn"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -2137,77 +2131,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rknn-toolkit-lite2"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "psutil" },
|
||||
{ name = "ruamel-yaml" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/77/6af374a4a8cd2aee762a1fb8a3050dcf3f129134bbdc4bb6bed755c4325b/rknn_toolkit_lite2-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b6733689bd09a262bcb6ba4744e690dd4b37ebeac4ed427cf45242c4b4ce9a4", size = 559372 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/0c/76ff1eb09d09ce4394a6959d2343a321d28dd9e604348ffdafceafdc344c/rknn_toolkit_lite2-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e4fefe355dc34a155680e4bcb9e4abb37ebc271f045ec9e0a4a3a018bc5beb", size = 569149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/6e/8679562028051b02312212defc6e8c07248953f10dd7ad506e941b575bf3/rknn_toolkit_lite2-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37394371d1561f470c553f39869d7c35ff93405dffe3d0d72babf297a2b0aee9", size = 527457 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruamel-yaml"
|
||||
version = "0.18.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruamel-yaml-clib"
|
||||
version = "0.2.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.9"
|
||||
|
||||
@@ -67,7 +67,7 @@ custom_lint:
|
||||
- lib/entities/*.entity.dart
|
||||
- lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart
|
||||
- lib/infrastructure/entities/*.entity.dart
|
||||
- lib/infrastructure/repositories/{store,db,log,exif}.repository.dart
|
||||
- lib/infrastructure/repositories/*.repository.dart
|
||||
- lib/providers/infrastructure/db.provider.dart
|
||||
# acceptable exceptions for the time being (until Isar is fully replaced)
|
||||
- lib/providers/app_life_cycle.provider.dart
|
||||
@@ -93,6 +93,7 @@ custom_lint:
|
||||
- lib/infrastructure/utils/*.converter.dart
|
||||
# acceptable exceptions for the time being
|
||||
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
|
||||
- lib/infrastructure/utils/*.converter.dart
|
||||
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
|
||||
|
||||
24
mobile/lib/domain/interfaces/user.interface.dart
Normal file
24
mobile/lib/domain/interfaces/user.interface.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
abstract interface class IUserRepository implements IDatabaseRepository {
|
||||
Future<bool> insert(UserDto user);
|
||||
|
||||
Future<UserDto?> get(int id);
|
||||
|
||||
Future<UserDto?> getByUserId(String id);
|
||||
|
||||
Future<List<UserDto?>> getByUserIds(List<String> ids);
|
||||
|
||||
Future<List<UserDto>> getAll({SortUserBy? sortBy});
|
||||
|
||||
Future<bool> updateAll(List<UserDto> users);
|
||||
|
||||
Future<UserDto> update(UserDto user);
|
||||
|
||||
Future<void> delete(List<int> ids);
|
||||
|
||||
Future<void> deleteAll();
|
||||
}
|
||||
|
||||
enum SortUserBy { id }
|
||||
15
mobile/lib/domain/interfaces/user_api.repository.dart
Normal file
15
mobile/lib/domain/interfaces/user_api.repository.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
abstract interface class IUserApiRepository {
|
||||
Future<UserDto?> getMyUser();
|
||||
|
||||
Future<List<UserDto>> getAll();
|
||||
|
||||
/// Saves the [data] in the server and uses it as the current users profile image
|
||||
Future<String> createProfileImage({
|
||||
required String name,
|
||||
required Uint8List data,
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
/// Key for each possible value in the `Store`.
|
||||
/// Defines the data type for each value
|
||||
enum StoreKey<T> {
|
||||
version<int>._(0),
|
||||
assetETag<String>._(1),
|
||||
currentUser<User>._(2),
|
||||
currentUser<UserDto>._(2),
|
||||
deviceIdHash<int>._(3),
|
||||
deviceId<String>._(4),
|
||||
backupFailedSince<DateTime>._(5),
|
||||
|
||||
157
mobile/lib/domain/models/user.model.dart
Normal file
157
mobile/lib/domain/models/user.model.dart
Normal file
@@ -0,0 +1,157 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
|
||||
enum AvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary,
|
||||
pink,
|
||||
red,
|
||||
yellow,
|
||||
blue,
|
||||
green,
|
||||
purple,
|
||||
orange,
|
||||
gray,
|
||||
amber;
|
||||
|
||||
Color toColor({bool isDarkTheme = false}) => switch (this) {
|
||||
AvatarColor.primary =>
|
||||
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
|
||||
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
|
||||
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
|
||||
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
|
||||
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
|
||||
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
|
||||
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
|
||||
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
|
||||
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
|
||||
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Rename to User once Isar is removed
|
||||
class UserDto {
|
||||
final String uid;
|
||||
final String email;
|
||||
final String name;
|
||||
final bool isAdmin;
|
||||
final DateTime updatedAt;
|
||||
|
||||
final String? profileImagePath;
|
||||
final AvatarColor avatarColor;
|
||||
|
||||
final bool memoryEnabled;
|
||||
final bool inTimeline;
|
||||
|
||||
final bool isPartnerSharedBy;
|
||||
final bool isPartnerSharedWith;
|
||||
|
||||
final int quotaUsageInBytes;
|
||||
final int quotaSizeInBytes;
|
||||
|
||||
int get id => fastHash(uid);
|
||||
bool get hasQuota => quotaSizeInBytes > 0;
|
||||
|
||||
const UserDto({
|
||||
required this.uid,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.isAdmin,
|
||||
required this.updatedAt,
|
||||
this.profileImagePath,
|
||||
this.avatarColor = AvatarColor.primary,
|
||||
this.memoryEnabled = true,
|
||||
this.inTimeline = false,
|
||||
this.isPartnerSharedBy = false,
|
||||
this.isPartnerSharedWith = false,
|
||||
this.quotaUsageInBytes = 0,
|
||||
this.quotaSizeInBytes = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''User: {
|
||||
id: $id,
|
||||
uid: $uid,
|
||||
email: $email,
|
||||
name: $name,
|
||||
isAdmin: $isAdmin,
|
||||
updatedAt: $updatedAt,
|
||||
profileImagePath: ${profileImagePath ?? '<NA>'},
|
||||
avatarColor: $avatarColor,
|
||||
memoryEnabled: $memoryEnabled,
|
||||
inTimeline: $inTimeline,
|
||||
isPartnerSharedBy: $isPartnerSharedBy,
|
||||
isPartnerSharedWith: $isPartnerSharedWith,
|
||||
quotaUsageInBytes: $quotaUsageInBytes,
|
||||
quotaSizeInBytes: $quotaSizeInBytes,
|
||||
}''';
|
||||
}
|
||||
|
||||
UserDto copyWith({
|
||||
String? uid,
|
||||
String? email,
|
||||
String? name,
|
||||
bool? isAdmin,
|
||||
DateTime? updatedAt,
|
||||
String? profileImagePath,
|
||||
AvatarColor? avatarColor,
|
||||
bool? memoryEnabled,
|
||||
bool? inTimeline,
|
||||
bool? isPartnerSharedBy,
|
||||
bool? isPartnerSharedWith,
|
||||
int? quotaUsageInBytes,
|
||||
int? quotaSizeInBytes,
|
||||
}) =>
|
||||
UserDto(
|
||||
uid: uid ?? this.uid,
|
||||
email: email ?? this.email,
|
||||
name: name ?? this.name,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||
avatarColor: avatarColor ?? this.avatarColor,
|
||||
memoryEnabled: memoryEnabled ?? this.memoryEnabled,
|
||||
inTimeline: inTimeline ?? this.inTimeline,
|
||||
isPartnerSharedBy: isPartnerSharedBy ?? this.isPartnerSharedBy,
|
||||
isPartnerSharedWith: isPartnerSharedWith ?? this.isPartnerSharedWith,
|
||||
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
|
||||
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(covariant UserDto other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.uid == uid &&
|
||||
other.updatedAt.isAtSameMomentAs(updatedAt) &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.email == email &&
|
||||
other.name == name &&
|
||||
other.isPartnerSharedBy == isPartnerSharedBy &&
|
||||
other.isPartnerSharedWith == isPartnerSharedWith &&
|
||||
other.profileImagePath == profileImagePath &&
|
||||
other.isAdmin == isAdmin &&
|
||||
other.memoryEnabled == memoryEnabled &&
|
||||
other.inTimeline == inTimeline &&
|
||||
other.quotaUsageInBytes == quotaUsageInBytes &&
|
||||
other.quotaSizeInBytes == quotaSizeInBytes;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
uid.hashCode ^
|
||||
name.hashCode ^
|
||||
email.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
isAdmin.hashCode ^
|
||||
profileImagePath.hashCode ^
|
||||
avatarColor.hashCode ^
|
||||
memoryEnabled.hashCode ^
|
||||
inTimeline.hashCode ^
|
||||
isPartnerSharedBy.hashCode ^
|
||||
isPartnerSharedWith.hashCode ^
|
||||
quotaUsageInBytes.hashCode ^
|
||||
quotaSizeInBytes.hashCode;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ class StoreService {
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Asynchronously stores the value in the DB and synchronously in the cache
|
||||
/// Asynchronously stores the value in the Store
|
||||
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
|
||||
if (_cache[key.id] == value) return;
|
||||
await _storeRepository.insert(key, value);
|
||||
@@ -84,7 +84,7 @@ class StoreService {
|
||||
/// Watches a specific key for changes
|
||||
Stream<T?> watch<T>(StoreKey<T> key) => _storeRepository.watch(key);
|
||||
|
||||
/// Removes the value asynchronously from the DB and synchronously from the cache
|
||||
/// Removes the value asynchronously from the Store
|
||||
Future<void> delete<T>(StoreKey<T> key) async {
|
||||
await _storeRepository.delete(key);
|
||||
_cache.remove(key.id);
|
||||
|
||||
64
mobile/lib/domain/services/user.service.dart
Normal file
64
mobile/lib/domain/services/user.service.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class UserService {
|
||||
final Logger _log = Logger("UserService");
|
||||
final IUserRepository _userRepository;
|
||||
final IUserApiRepository _userApiRepository;
|
||||
final StoreService _storeService;
|
||||
|
||||
UserService({
|
||||
required IUserRepository userRepository,
|
||||
required IUserApiRepository userApiRepository,
|
||||
required StoreService storeService,
|
||||
}) : _userRepository = userRepository,
|
||||
_userApiRepository = userApiRepository,
|
||||
_storeService = storeService;
|
||||
|
||||
UserDto getMyUser() {
|
||||
return _storeService.get(StoreKey.currentUser);
|
||||
}
|
||||
|
||||
UserDto? tryGetMyUser() {
|
||||
return _storeService.tryGet(StoreKey.currentUser);
|
||||
}
|
||||
|
||||
Stream<UserDto?> watchMyUser() {
|
||||
return _storeService.watch(StoreKey.currentUser);
|
||||
}
|
||||
|
||||
Future<UserDto?> refreshMyUser() async {
|
||||
final user = await _userApiRepository.getMyUser();
|
||||
if (user == null) return null;
|
||||
await _storeService.put(StoreKey.currentUser, user);
|
||||
await _userRepository.update(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<String?> createProfileImage(String name, Uint8List image) async {
|
||||
try {
|
||||
return await _userApiRepository.createProfileImage(
|
||||
name: name,
|
||||
data: image,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to upload profile image", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<UserDto>> getAll() async {
|
||||
return await _userRepository.getAll();
|
||||
}
|
||||
|
||||
Future<void> deleteAll() {
|
||||
return _userRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
// ignore: implementation_imports
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
part 'user.entity.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class User {
|
||||
User({
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.isAdmin,
|
||||
this.isPartnerSharedBy = false,
|
||||
this.isPartnerSharedWith = false,
|
||||
this.profileImagePath = '',
|
||||
this.avatarColor = AvatarColorEnum.primary,
|
||||
this.memoryEnabled = true,
|
||||
this.inTimeline = false,
|
||||
this.quotaUsageInBytes = 0,
|
||||
this.quotaSizeInBytes = 0,
|
||||
});
|
||||
|
||||
Id get isarId => fastHash(id);
|
||||
|
||||
User.fromUserDto(
|
||||
UserAdminResponseDto dto,
|
||||
UserPreferencesResponseDto? preferences,
|
||||
) : id = dto.id,
|
||||
updatedAt = dto.updatedAt,
|
||||
email = dto.email,
|
||||
name = dto.name,
|
||||
isPartnerSharedBy = false,
|
||||
isPartnerSharedWith = false,
|
||||
profileImagePath = dto.profileImagePath,
|
||||
isAdmin = dto.isAdmin,
|
||||
memoryEnabled = preferences?.memories.enabled ?? false,
|
||||
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||
inTimeline = false,
|
||||
quotaUsageInBytes = dto.quotaUsageInBytes ?? 0,
|
||||
quotaSizeInBytes = dto.quotaSizeInBytes ?? 0;
|
||||
|
||||
User.fromPartnerDto(PartnerResponseDto dto)
|
||||
: id = dto.id,
|
||||
updatedAt = DateTime.now(),
|
||||
email = dto.email,
|
||||
name = dto.name,
|
||||
isPartnerSharedBy = false,
|
||||
isPartnerSharedWith = false,
|
||||
profileImagePath = dto.profileImagePath,
|
||||
isAdmin = false,
|
||||
memoryEnabled = false,
|
||||
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||
inTimeline = dto.inTimeline ?? false,
|
||||
quotaUsageInBytes = 0,
|
||||
quotaSizeInBytes = 0;
|
||||
|
||||
/// Base user dto used where the complete user object is not required
|
||||
User.fromSimpleUserDto(UserResponseDto dto)
|
||||
: id = dto.id,
|
||||
email = dto.email,
|
||||
name = dto.name,
|
||||
profileImagePath = dto.profileImagePath,
|
||||
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||
// Fill the remaining fields with placeholders
|
||||
isAdmin = false,
|
||||
inTimeline = false,
|
||||
memoryEnabled = false,
|
||||
isPartnerSharedBy = false,
|
||||
isPartnerSharedWith = false,
|
||||
updatedAt = DateTime.now(),
|
||||
quotaUsageInBytes = 0,
|
||||
quotaSizeInBytes = 0;
|
||||
|
||||
@Index(unique: true, replace: false, type: IndexType.hash)
|
||||
String id;
|
||||
DateTime updatedAt;
|
||||
String email;
|
||||
String name;
|
||||
bool isPartnerSharedBy;
|
||||
bool isPartnerSharedWith;
|
||||
bool isAdmin;
|
||||
String profileImagePath;
|
||||
@Enumerated(EnumType.ordinal)
|
||||
AvatarColorEnum avatarColor;
|
||||
bool memoryEnabled;
|
||||
bool inTimeline;
|
||||
int quotaUsageInBytes;
|
||||
int quotaSizeInBytes;
|
||||
|
||||
bool get hasQuota => quotaSizeInBytes > 0;
|
||||
@Backlink(to: 'owner')
|
||||
final IsarLinks<Album> albums = IsarLinks<Album>();
|
||||
@Backlink(to: 'sharedUsers')
|
||||
final IsarLinks<Album> sharedAlbums = IsarLinks<Album>();
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! User) return false;
|
||||
return id == other.id &&
|
||||
updatedAt.isAtSameMomentAs(other.updatedAt) &&
|
||||
avatarColor == other.avatarColor &&
|
||||
email == other.email &&
|
||||
name == other.name &&
|
||||
isPartnerSharedBy == other.isPartnerSharedBy &&
|
||||
isPartnerSharedWith == other.isPartnerSharedWith &&
|
||||
profileImagePath == other.profileImagePath &&
|
||||
isAdmin == other.isAdmin &&
|
||||
memoryEnabled == other.memoryEnabled &&
|
||||
inTimeline == other.inTimeline &&
|
||||
quotaUsageInBytes == other.quotaUsageInBytes &&
|
||||
quotaSizeInBytes == other.quotaSizeInBytes;
|
||||
}
|
||||
|
||||
@override
|
||||
@ignore
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
email.hashCode ^
|
||||
name.hashCode ^
|
||||
isPartnerSharedBy.hashCode ^
|
||||
isPartnerSharedWith.hashCode ^
|
||||
profileImagePath.hashCode ^
|
||||
avatarColor.hashCode ^
|
||||
isAdmin.hashCode ^
|
||||
memoryEnabled.hashCode ^
|
||||
inTimeline.hashCode ^
|
||||
quotaUsageInBytes.hashCode ^
|
||||
quotaSizeInBytes.hashCode;
|
||||
}
|
||||
|
||||
enum AvatarColorEnum {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary,
|
||||
pink,
|
||||
red,
|
||||
yellow,
|
||||
blue,
|
||||
green,
|
||||
purple,
|
||||
orange,
|
||||
gray,
|
||||
amber,
|
||||
}
|
||||
|
||||
extension AvatarColorEnumHelper on UserAvatarColor {
|
||||
AvatarColorEnum toAvatarColor() => switch (this) {
|
||||
UserAvatarColor.primary => AvatarColorEnum.primary,
|
||||
UserAvatarColor.pink => AvatarColorEnum.pink,
|
||||
UserAvatarColor.red => AvatarColorEnum.red,
|
||||
UserAvatarColor.yellow => AvatarColorEnum.yellow,
|
||||
UserAvatarColor.blue => AvatarColorEnum.blue,
|
||||
UserAvatarColor.green => AvatarColorEnum.green,
|
||||
UserAvatarColor.purple => AvatarColorEnum.purple,
|
||||
UserAvatarColor.orange => AvatarColorEnum.orange,
|
||||
UserAvatarColor.gray => AvatarColorEnum.gray,
|
||||
UserAvatarColor.amber => AvatarColorEnum.amber,
|
||||
_ => AvatarColorEnum.primary,
|
||||
};
|
||||
}
|
||||
|
||||
extension AvatarColorToColorHelper on AvatarColorEnum {
|
||||
Color toColor([bool isDarkTheme = false]) => switch (this) {
|
||||
AvatarColorEnum.primary =>
|
||||
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
|
||||
AvatarColorEnum.pink => const Color.fromARGB(255, 244, 114, 182),
|
||||
AvatarColorEnum.red => const Color.fromARGB(255, 239, 68, 68),
|
||||
AvatarColorEnum.yellow => const Color.fromARGB(255, 234, 179, 8),
|
||||
AvatarColorEnum.blue => const Color.fromARGB(255, 59, 130, 246),
|
||||
AvatarColorEnum.green => const Color.fromARGB(255, 22, 163, 74),
|
||||
AvatarColorEnum.purple => const Color.fromARGB(255, 147, 51, 234),
|
||||
AvatarColorEnum.orange => const Color.fromARGB(255, 234, 88, 12),
|
||||
AvatarColorEnum.gray => const Color.fromARGB(255, 75, 85, 99),
|
||||
AvatarColorEnum.amber => const Color.fromARGB(255, 217, 119, 6),
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
||||
extension ListExtension<E> on List<E> {
|
||||
List<E> uniqueConsecutive({
|
||||
@@ -58,11 +58,11 @@ extension AssetListExtension on Iterable<Asset> {
|
||||
/// Returns the assets that are owned by the user passed to the [owner] param
|
||||
/// If [owner] is null, an empty list is returned
|
||||
Iterable<Asset> ownedOnly(
|
||||
User? owner, {
|
||||
UserDto? owner, {
|
||||
void Function()? errorCallback,
|
||||
}) {
|
||||
if (owner == null) return [];
|
||||
final userId = owner.isarId;
|
||||
final userId = owner.id;
|
||||
final bool onlyOwned = every((e) => e.ownerId == userId);
|
||||
if (!onlyOwned) {
|
||||
if (errorCallback != null) errorCallback();
|
||||
|
||||
73
mobile/lib/infrastructure/entities/user.entity.dart
Normal file
73
mobile/lib/infrastructure/entities/user.entity.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'user.entity.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class User {
|
||||
Id get isarId => fastHash(id);
|
||||
@Index(unique: true, replace: false, type: IndexType.hash)
|
||||
final String id;
|
||||
final DateTime updatedAt;
|
||||
final String email;
|
||||
final String name;
|
||||
final bool isPartnerSharedBy;
|
||||
final bool isPartnerSharedWith;
|
||||
final bool isAdmin;
|
||||
final String profileImagePath;
|
||||
@Enumerated(EnumType.ordinal)
|
||||
final AvatarColor avatarColor;
|
||||
final bool memoryEnabled;
|
||||
final bool inTimeline;
|
||||
final int quotaUsageInBytes;
|
||||
final int quotaSizeInBytes;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.isAdmin,
|
||||
this.isPartnerSharedBy = false,
|
||||
this.isPartnerSharedWith = false,
|
||||
this.profileImagePath = '',
|
||||
this.avatarColor = AvatarColor.primary,
|
||||
this.memoryEnabled = true,
|
||||
this.inTimeline = false,
|
||||
this.quotaUsageInBytes = 0,
|
||||
this.quotaSizeInBytes = 0,
|
||||
});
|
||||
|
||||
static User fromDto(UserDto dto) => User(
|
||||
id: dto.uid,
|
||||
updatedAt: dto.updatedAt,
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
isAdmin: dto.isAdmin,
|
||||
isPartnerSharedBy: dto.isPartnerSharedBy,
|
||||
isPartnerSharedWith: dto.isPartnerSharedWith,
|
||||
profileImagePath: dto.profileImagePath ?? "",
|
||||
avatarColor: dto.avatarColor,
|
||||
memoryEnabled: dto.memoryEnabled,
|
||||
inTimeline: dto.inTimeline,
|
||||
quotaUsageInBytes: dto.quotaUsageInBytes,
|
||||
quotaSizeInBytes: dto.quotaSizeInBytes,
|
||||
);
|
||||
|
||||
UserDto toDto() => UserDto(
|
||||
uid: id,
|
||||
email: email,
|
||||
name: name,
|
||||
isAdmin: isAdmin,
|
||||
updatedAt: updatedAt,
|
||||
profileImagePath: profileImagePath.isEmpty ? null : profileImagePath,
|
||||
avatarColor: avatarColor,
|
||||
memoryEnabled: memoryEnabled,
|
||||
inTimeline: inTimeline,
|
||||
isPartnerSharedBy: isPartnerSharedBy,
|
||||
isPartnerSharedWith: isPartnerSharedWith,
|
||||
quotaUsageInBytes: quotaUsageInBytes,
|
||||
quotaSizeInBytes: quotaSizeInBytes,
|
||||
);
|
||||
}
|
||||
@@ -28,63 +28,58 @@ const UserSchema = CollectionSchema(
|
||||
name: r'email',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'hasQuota': PropertySchema(
|
||||
id: 2,
|
||||
name: r'hasQuota',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'id': PropertySchema(
|
||||
id: 3,
|
||||
id: 2,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'inTimeline': PropertySchema(
|
||||
id: 4,
|
||||
id: 3,
|
||||
name: r'inTimeline',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isAdmin': PropertySchema(
|
||||
id: 5,
|
||||
id: 4,
|
||||
name: r'isAdmin',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isPartnerSharedBy': PropertySchema(
|
||||
id: 6,
|
||||
id: 5,
|
||||
name: r'isPartnerSharedBy',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isPartnerSharedWith': PropertySchema(
|
||||
id: 7,
|
||||
id: 6,
|
||||
name: r'isPartnerSharedWith',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'memoryEnabled': PropertySchema(
|
||||
id: 8,
|
||||
id: 7,
|
||||
name: r'memoryEnabled',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'name': PropertySchema(
|
||||
id: 9,
|
||||
id: 8,
|
||||
name: r'name',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'profileImagePath': PropertySchema(
|
||||
id: 10,
|
||||
id: 9,
|
||||
name: r'profileImagePath',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'quotaSizeInBytes': PropertySchema(
|
||||
id: 11,
|
||||
id: 10,
|
||||
name: r'quotaSizeInBytes',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'quotaUsageInBytes': PropertySchema(
|
||||
id: 12,
|
||||
id: 11,
|
||||
name: r'quotaUsageInBytes',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 13,
|
||||
id: 12,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
)
|
||||
@@ -109,22 +104,7 @@ const UserSchema = CollectionSchema(
|
||||
],
|
||||
)
|
||||
},
|
||||
links: {
|
||||
r'albums': LinkSchema(
|
||||
id: -8764917375410137318,
|
||||
name: r'albums',
|
||||
target: r'Album',
|
||||
single: false,
|
||||
linkName: r'owner',
|
||||
),
|
||||
r'sharedAlbums': LinkSchema(
|
||||
id: -7037628715076287024,
|
||||
name: r'sharedAlbums',
|
||||
target: r'Album',
|
||||
single: false,
|
||||
linkName: r'sharedUsers',
|
||||
)
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _userGetId,
|
||||
getLinks: _userGetLinks,
|
||||
@@ -153,18 +133,17 @@ void _userSerialize(
|
||||
) {
|
||||
writer.writeByte(offsets[0], object.avatarColor.index);
|
||||
writer.writeString(offsets[1], object.email);
|
||||
writer.writeBool(offsets[2], object.hasQuota);
|
||||
writer.writeString(offsets[3], object.id);
|
||||
writer.writeBool(offsets[4], object.inTimeline);
|
||||
writer.writeBool(offsets[5], object.isAdmin);
|
||||
writer.writeBool(offsets[6], object.isPartnerSharedBy);
|
||||
writer.writeBool(offsets[7], object.isPartnerSharedWith);
|
||||
writer.writeBool(offsets[8], object.memoryEnabled);
|
||||
writer.writeString(offsets[9], object.name);
|
||||
writer.writeString(offsets[10], object.profileImagePath);
|
||||
writer.writeLong(offsets[11], object.quotaSizeInBytes);
|
||||
writer.writeLong(offsets[12], object.quotaUsageInBytes);
|
||||
writer.writeDateTime(offsets[13], object.updatedAt);
|
||||
writer.writeString(offsets[2], object.id);
|
||||
writer.writeBool(offsets[3], object.inTimeline);
|
||||
writer.writeBool(offsets[4], object.isAdmin);
|
||||
writer.writeBool(offsets[5], object.isPartnerSharedBy);
|
||||
writer.writeBool(offsets[6], object.isPartnerSharedWith);
|
||||
writer.writeBool(offsets[7], object.memoryEnabled);
|
||||
writer.writeString(offsets[8], object.name);
|
||||
writer.writeString(offsets[9], object.profileImagePath);
|
||||
writer.writeLong(offsets[10], object.quotaSizeInBytes);
|
||||
writer.writeLong(offsets[11], object.quotaUsageInBytes);
|
||||
writer.writeDateTime(offsets[12], object.updatedAt);
|
||||
}
|
||||
|
||||
User _userDeserialize(
|
||||
@@ -176,19 +155,19 @@ User _userDeserialize(
|
||||
final object = User(
|
||||
avatarColor:
|
||||
_UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ??
|
||||
AvatarColorEnum.primary,
|
||||
AvatarColor.primary,
|
||||
email: reader.readString(offsets[1]),
|
||||
id: reader.readString(offsets[3]),
|
||||
inTimeline: reader.readBoolOrNull(offsets[4]) ?? false,
|
||||
isAdmin: reader.readBool(offsets[5]),
|
||||
isPartnerSharedBy: reader.readBoolOrNull(offsets[6]) ?? false,
|
||||
isPartnerSharedWith: reader.readBoolOrNull(offsets[7]) ?? false,
|
||||
memoryEnabled: reader.readBoolOrNull(offsets[8]) ?? true,
|
||||
name: reader.readString(offsets[9]),
|
||||
profileImagePath: reader.readStringOrNull(offsets[10]) ?? '',
|
||||
quotaSizeInBytes: reader.readLongOrNull(offsets[11]) ?? 0,
|
||||
quotaUsageInBytes: reader.readLongOrNull(offsets[12]) ?? 0,
|
||||
updatedAt: reader.readDateTime(offsets[13]),
|
||||
id: reader.readString(offsets[2]),
|
||||
inTimeline: reader.readBoolOrNull(offsets[3]) ?? false,
|
||||
isAdmin: reader.readBool(offsets[4]),
|
||||
isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false,
|
||||
isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false,
|
||||
memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true,
|
||||
name: reader.readString(offsets[8]),
|
||||
profileImagePath: reader.readStringOrNull(offsets[9]) ?? '',
|
||||
quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0,
|
||||
quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0,
|
||||
updatedAt: reader.readDateTime(offsets[12]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
@@ -202,32 +181,30 @@ P _userDeserializeProp<P>(
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AvatarColorEnum.primary) as P;
|
||||
AvatarColor.primary) as P;
|
||||
case 1:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 2:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 3:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 4:
|
||||
case 3:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 5:
|
||||
case 4:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 5:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 6:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 7:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 8:
|
||||
return (reader.readBoolOrNull(offset) ?? true) as P;
|
||||
case 9:
|
||||
case 8:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 10:
|
||||
case 9:
|
||||
return (reader.readStringOrNull(offset) ?? '') as P;
|
||||
case 10:
|
||||
return (reader.readLongOrNull(offset) ?? 0) as P;
|
||||
case 11:
|
||||
return (reader.readLongOrNull(offset) ?? 0) as P;
|
||||
case 12:
|
||||
return (reader.readLongOrNull(offset) ?? 0) as P;
|
||||
case 13:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
@@ -247,16 +224,16 @@ const _UseravatarColorEnumValueMap = {
|
||||
'amber': 9,
|
||||
};
|
||||
const _UseravatarColorValueEnumMap = {
|
||||
0: AvatarColorEnum.primary,
|
||||
1: AvatarColorEnum.pink,
|
||||
2: AvatarColorEnum.red,
|
||||
3: AvatarColorEnum.yellow,
|
||||
4: AvatarColorEnum.blue,
|
||||
5: AvatarColorEnum.green,
|
||||
6: AvatarColorEnum.purple,
|
||||
7: AvatarColorEnum.orange,
|
||||
8: AvatarColorEnum.gray,
|
||||
9: AvatarColorEnum.amber,
|
||||
0: AvatarColor.primary,
|
||||
1: AvatarColor.pink,
|
||||
2: AvatarColor.red,
|
||||
3: AvatarColor.yellow,
|
||||
4: AvatarColor.blue,
|
||||
5: AvatarColor.green,
|
||||
6: AvatarColor.purple,
|
||||
7: AvatarColor.orange,
|
||||
8: AvatarColor.gray,
|
||||
9: AvatarColor.amber,
|
||||
};
|
||||
|
||||
Id _userGetId(User object) {
|
||||
@@ -264,14 +241,10 @@ Id _userGetId(User object) {
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _userGetLinks(User object) {
|
||||
return [object.albums, object.sharedAlbums];
|
||||
return [];
|
||||
}
|
||||
|
||||
void _userAttach(IsarCollection<dynamic> col, Id id, User object) {
|
||||
object.albums.attach(col, col.isar.collection<Album>(), r'albums', id);
|
||||
object.sharedAlbums
|
||||
.attach(col, col.isar.collection<Album>(), r'sharedAlbums', id);
|
||||
}
|
||||
void _userAttach(IsarCollection<dynamic> col, Id id, User object) {}
|
||||
|
||||
extension UserByIndex on IsarCollection<User> {
|
||||
Future<User?> getById(String id) {
|
||||
@@ -447,7 +420,7 @@ extension UserQueryWhere on QueryBuilder<User, User, QWhereClause> {
|
||||
|
||||
extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorEqualTo(
|
||||
AvatarColorEnum value) {
|
||||
AvatarColor value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'avatarColor',
|
||||
@@ -457,7 +430,7 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorGreaterThan(
|
||||
AvatarColorEnum value, {
|
||||
AvatarColor value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -470,7 +443,7 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorLessThan(
|
||||
AvatarColorEnum value, {
|
||||
AvatarColor value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -483,8 +456,8 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorBetween(
|
||||
AvatarColorEnum lower,
|
||||
AvatarColorEnum upper, {
|
||||
AvatarColor lower,
|
||||
AvatarColor upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
@@ -627,15 +600,6 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> hasQuotaEqualTo(bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hasQuota',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> idEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
@@ -1285,118 +1249,7 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
|
||||
extension UserQueryObject on QueryBuilder<User, User, QFilterCondition> {}
|
||||
|
||||
extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albums(FilterQuery<Album> q) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.link(q, r'albums');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthEqualTo(
|
||||
int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'albums', length, true, length, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albumsIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'albums', 0, true, 0, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albumsIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'albums', 0, false, 999999, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'albums', 0, true, length, include);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'albums', length, include, 999999, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(
|
||||
r'albums', lower, includeLower, upper, includeUpper);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbums(
|
||||
FilterQuery<Album> q) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.link(q, r'sharedAlbums');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthEqualTo(
|
||||
int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'sharedAlbums', length, true, length, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'sharedAlbums', 0, true, 0, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'sharedAlbums', 0, false, 999999, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'sharedAlbums', 0, true, length, include);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'sharedAlbums', length, include, 999999, true);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(
|
||||
r'sharedAlbums', lower, includeLower, upper, includeUpper);
|
||||
});
|
||||
}
|
||||
}
|
||||
extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {}
|
||||
|
||||
extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
|
||||
QueryBuilder<User, User, QAfterSortBy> sortByAvatarColor() {
|
||||
@@ -1423,18 +1276,6 @@ extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> sortByHasQuota() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hasQuota', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> sortByHasQuotaDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hasQuota', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> sortById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
@@ -1593,18 +1434,6 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> thenByHasQuota() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hasQuota', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> thenByHasQuotaDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hasQuota', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
@@ -1764,12 +1593,6 @@ extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QDistinct> distinctByHasQuota() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hasQuota');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QDistinct> distinctById(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -1848,7 +1671,7 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, AvatarColorEnum, QQueryOperations> avatarColorProperty() {
|
||||
QueryBuilder<User, AvatarColor, QQueryOperations> avatarColorProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'avatarColor');
|
||||
});
|
||||
@@ -1860,12 +1683,6 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, bool, QQueryOperations> hasQuotaProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hasQuota');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, String, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
11
mobile/lib/infrastructure/repositories/api.repository.dart
Normal file
11
mobile/lib/infrastructure/repositories/api.repository.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:immich_mobile/constants/errors.dart';
|
||||
|
||||
class ApiRepository {
|
||||
const ApiRepository();
|
||||
|
||||
Future<T> checkNull<T>(Future<T?> future) async {
|
||||
final response = await future;
|
||||
if (response == null) throw NoResponseDtoError();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class IsarStoreRepository extends IsarDatabaseRepository
|
||||
@@ -78,7 +78,7 @@ class IsarStoreRepository extends IsarDatabaseRepository
|
||||
const (DateTime) => entity.intValue == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
|
||||
const (User) => await UserRepository(_db).getByDbId(entity.intValue!),
|
||||
const (UserDto) => await IsarUserRepository(_db).get(entity.intValue!),
|
||||
_ => null,
|
||||
} as T?;
|
||||
|
||||
@@ -88,8 +88,8 @@ class IsarStoreRepository extends IsarDatabaseRepository
|
||||
const (String) => (null, value as String),
|
||||
const (bool) => ((value as bool) ? 1 : 0, null),
|
||||
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
|
||||
const (User) => (
|
||||
(await UserRepository(_db).update(value as User)).isarId,
|
||||
const (UserDto) => (
|
||||
(await IsarUserRepository(_db).update(value as UserDto)).id,
|
||||
null,
|
||||
),
|
||||
_ => throw UnsupportedError(
|
||||
|
||||
80
mobile/lib/infrastructure/repositories/user.repository.dart
Normal file
80
mobile/lib/infrastructure/repositories/user.repository.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
||||
as entity;
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class IsarUserRepository extends IsarDatabaseRepository
|
||||
implements IUserRepository {
|
||||
final Isar _db;
|
||||
const IsarUserRepository(super.db) : _db = db;
|
||||
|
||||
@override
|
||||
Future<void> delete(List<int> ids) async {
|
||||
await transaction(() async {
|
||||
await _db.users.deleteAll(ids);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAll() async {
|
||||
await transaction(() async {
|
||||
await _db.users.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserDto?> get(int id) async {
|
||||
return (await _db.users.get(id))?.toDto();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
|
||||
return (await _db.users
|
||||
.where()
|
||||
.optional(
|
||||
sortBy != null,
|
||||
(query) => switch (sortBy!) {
|
||||
SortUserBy.id => query.sortById(),
|
||||
},
|
||||
)
|
||||
.findAll())
|
||||
.map((u) => u.toDto())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserDto?> getByUserId(String id) async {
|
||||
return (await _db.users.getById(id))?.toDto();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<UserDto?>> getByUserIds(List<String> ids) async {
|
||||
return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> insert(UserDto user) async {
|
||||
await transaction(() async {
|
||||
await _db.users.put(entity.User.fromDto(user));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserDto> update(UserDto user) async {
|
||||
await transaction(() async {
|
||||
await _db.users.put(entity.User.fromDto(user));
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> updateAll(List<UserDto> users) async {
|
||||
await transaction(() async {
|
||||
await _db.users.putAll(users.map(entity.User.fromDto).toList());
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class UserApiRepository extends ApiRepository implements IUserApiRepository {
|
||||
final UsersApi _api;
|
||||
const UserApiRepository(this._api);
|
||||
|
||||
@override
|
||||
Future<UserDto?> getMyUser() async {
|
||||
final (adminDto, preferenceDto) =
|
||||
await (_api.getMyUser(), _api.getMyPreferences()).wait;
|
||||
if (adminDto == null) return null;
|
||||
|
||||
return UserConverter.fromAdminDto(adminDto, preferenceDto);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> createProfileImage({
|
||||
required String name,
|
||||
required Uint8List data,
|
||||
}) async {
|
||||
final res = await checkNull(
|
||||
_api.createProfileImage(
|
||||
MultipartFile.fromBytes('file', data, filename: name),
|
||||
),
|
||||
);
|
||||
return res.profileImagePath;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<UserDto>> getAll() async {
|
||||
final dto = await checkNull(_api.searchUsers());
|
||||
return dto.map(UserConverter.fromSimpleUserDto).toList();
|
||||
}
|
||||
}
|
||||
66
mobile/lib/infrastructure/utils/user.converter.dart
Normal file
66
mobile/lib/infrastructure/utils/user.converter.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
abstract final class UserConverter {
|
||||
/// Base user dto used where the complete user object is not required
|
||||
static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto(
|
||||
uid: dto.id,
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
isAdmin: false,
|
||||
updatedAt: DateTime.now(),
|
||||
profileImagePath: dto.profileImagePath,
|
||||
avatarColor: dto.avatarColor.toAvatarColor(),
|
||||
);
|
||||
|
||||
static UserDto fromAdminDto(
|
||||
UserAdminResponseDto adminDto, [
|
||||
UserPreferencesResponseDto? preferenceDto,
|
||||
]) =>
|
||||
UserDto(
|
||||
uid: adminDto.id,
|
||||
email: adminDto.email,
|
||||
name: adminDto.name,
|
||||
isAdmin: adminDto.isAdmin,
|
||||
updatedAt: adminDto.updatedAt,
|
||||
profileImagePath: adminDto.profileImagePath,
|
||||
avatarColor: adminDto.avatarColor.toAvatarColor(),
|
||||
memoryEnabled: preferenceDto?.memories.enabled ?? true,
|
||||
inTimeline: false,
|
||||
isPartnerSharedBy: false,
|
||||
isPartnerSharedWith: false,
|
||||
quotaUsageInBytes: adminDto.quotaUsageInBytes ?? 0,
|
||||
quotaSizeInBytes: adminDto.quotaSizeInBytes ?? 0,
|
||||
);
|
||||
|
||||
static UserDto fromPartnerDto(PartnerResponseDto dto) => UserDto(
|
||||
uid: dto.id,
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
isAdmin: false,
|
||||
updatedAt: DateTime.now(),
|
||||
profileImagePath: dto.profileImagePath,
|
||||
avatarColor: dto.avatarColor.toAvatarColor(),
|
||||
memoryEnabled: false,
|
||||
inTimeline: dto.inTimeline ?? false,
|
||||
isPartnerSharedBy: false,
|
||||
isPartnerSharedWith: false,
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: 0,
|
||||
);
|
||||
}
|
||||
|
||||
extension on UserAvatarColor {
|
||||
AvatarColor toAvatarColor() => switch (this) {
|
||||
UserAvatarColor.red => AvatarColor.red,
|
||||
UserAvatarColor.green => AvatarColor.green,
|
||||
UserAvatarColor.blue => AvatarColor.blue,
|
||||
UserAvatarColor.purple => AvatarColor.purple,
|
||||
UserAvatarColor.orange => AvatarColor.orange,
|
||||
UserAvatarColor.pink => AvatarColor.pink,
|
||||
UserAvatarColor.amber => AvatarColor.amber,
|
||||
UserAvatarColor.yellow => AvatarColor.yellow,
|
||||
UserAvatarColor.gray => AvatarColor.gray,
|
||||
UserAvatarColor.primary || _ => AvatarColor.primary,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
|
||||
@@ -31,9 +31,9 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||
|
||||
Future<int> count({bool? local});
|
||||
|
||||
Future<void> addUsers(Album album, List<User> users);
|
||||
Future<void> addUsers(Album album, List<UserDto> users);
|
||||
|
||||
Future<void> removeUsers(Album album, List<User> users);
|
||||
Future<void> removeUsers(Album album, List<UserDto> users);
|
||||
|
||||
Future<void> addAssets(Album album, List<Asset> assets);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
abstract class IPartnerRepository {
|
||||
Future<List<User>> getSharedWith();
|
||||
Future<List<User>> getSharedBy();
|
||||
Stream<List<User>> watchSharedWith();
|
||||
Stream<List<User>> watchSharedBy();
|
||||
Future<List<UserDto>> getSharedWith();
|
||||
Future<List<UserDto>> getSharedBy();
|
||||
Stream<List<UserDto>> watchSharedWith();
|
||||
Stream<List<UserDto>> watchSharedBy();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
abstract interface class IPartnerApiRepository {
|
||||
Future<List<User>> getAll(Direction direction);
|
||||
Future<User> create(String id);
|
||||
Future<User> update(String id, {required bool inTimeline});
|
||||
Future<List<UserDto>> getAll(Direction direction);
|
||||
Future<UserDto> create(String id);
|
||||
Future<UserDto> update(String id, {required bool inTimeline});
|
||||
Future<void> delete(String id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IUserRepository implements IDatabaseRepository {
|
||||
Future<User?> get(String id);
|
||||
|
||||
Future<User?> getByDbId(int id);
|
||||
|
||||
Future<List<User>> getByIds(List<String> ids);
|
||||
|
||||
Future<List<User>> getAll({bool self = true, UserSort? sortBy});
|
||||
|
||||
/// Returns all users whose assets can be accessed (self+partners)
|
||||
Future<List<User>> getAllAccessible();
|
||||
|
||||
Future<List<User>> upsertAll(List<User> users);
|
||||
|
||||
Future<User> update(User user);
|
||||
|
||||
Future<void> deleteById(List<int> ids);
|
||||
|
||||
Future<User> me();
|
||||
|
||||
Future<void> clearTable();
|
||||
}
|
||||
|
||||
enum UserSort { id }
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
||||
abstract interface class IUserApiRepository {
|
||||
Future<List<User>> getAll();
|
||||
Future<({String profileImagePath})> createProfileImage({
|
||||
required String name,
|
||||
required Uint8List data,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
enum ActivityType { comment, like }
|
||||
|
||||
@@ -8,7 +8,7 @@ class Activity {
|
||||
final String? comment;
|
||||
final DateTime createdAt;
|
||||
final ActivityType type;
|
||||
final User user;
|
||||
final UserDto user;
|
||||
|
||||
const Activity({
|
||||
required this.id,
|
||||
@@ -25,7 +25,7 @@ class Activity {
|
||||
String? comment,
|
||||
DateTime? createdAt,
|
||||
ActivityType? type,
|
||||
User? user,
|
||||
UserDto? user,
|
||||
}) {
|
||||
return Activity(
|
||||
id: id ?? this.id,
|
||||
|
||||
@@ -3,11 +3,11 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -21,15 +21,15 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final AsyncValue<List<User>> suggestedShareUsers =
|
||||
final AsyncValue<List<UserDto>> suggestedShareUsers =
|
||||
ref.watch(otherUsersProvider);
|
||||
final sharedUsersList = useState<Set<User>>({});
|
||||
final sharedUsersList = useState<Set<UserDto>>({});
|
||||
|
||||
addNewUsersHandler() {
|
||||
context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
|
||||
context.maybePop(sharedUsersList.value.map((e) => e.uid).toList());
|
||||
}
|
||||
|
||||
buildTileIcon(User user) {
|
||||
buildTileIcon(UserDto user) {
|
||||
if (sharedUsersList.value.contains(user)) {
|
||||
return CircleAvatar(
|
||||
backgroundColor: context.primaryColor,
|
||||
@@ -45,7 +45,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
buildUserList(List<User> users) {
|
||||
buildUserList(List<UserDto> users) {
|
||||
List<Widget> usersChip = [];
|
||||
|
||||
for (var user in sharedUsersList.value) {
|
||||
@@ -151,7 +151,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
onData: (users) {
|
||||
for (var sharedUsers in album.sharedUsers) {
|
||||
users.removeWhere(
|
||||
(u) => u.id == sharedUsers.id || u.id == album.ownerId,
|
||||
(u) => u.uid == sharedUsers.id || u.uid == album.ownerId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
||||
as entity;
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
|
||||
@@ -26,7 +28,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final sharedUsers = useState(album.sharedUsers.toList());
|
||||
final sharedUsers =
|
||||
useState(album.sharedUsers.map((u) => u.toDto()).toList());
|
||||
final owner = album.owner.value;
|
||||
final userId = ref.watch(authProvider).userId;
|
||||
final activityEnabled = useState(album.activityEnabled);
|
||||
@@ -64,13 +67,13 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
|
||||
void removeUserFromAlbum(User user) async {
|
||||
void removeUserFromAlbum(UserDto user) async {
|
||||
isProcessing.value = true;
|
||||
|
||||
try {
|
||||
await ref.read(albumProvider.notifier).removeUser(album, user);
|
||||
album.sharedUsers.remove(user);
|
||||
sharedUsers.value = album.sharedUsers.toList();
|
||||
album.sharedUsers.remove(entity.User.fromDto(user));
|
||||
sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList();
|
||||
} catch (error) {
|
||||
showErrorMessage();
|
||||
}
|
||||
@@ -79,10 +82,10 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
|
||||
void handleUserClick(User user) {
|
||||
void handleUserClick(UserDto user) {
|
||||
var actions = [];
|
||||
|
||||
if (user.id == userId) {
|
||||
if (user.uid == userId) {
|
||||
actions = [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.exit_to_app_rounded),
|
||||
@@ -123,8 +126,9 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
|
||||
buildOwnerInfo() {
|
||||
return ListTile(
|
||||
leading:
|
||||
owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
|
||||
leading: owner != null
|
||||
? UserCircleAvatar(user: owner.toDto())
|
||||
: const SizedBox(),
|
||||
title: Text(
|
||||
album.owner.value?.name ?? "",
|
||||
style: const TextStyle(
|
||||
@@ -166,10 +170,10 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
trailing: userId == user.id || isOwner
|
||||
trailing: userId == user.uid || isOwner
|
||||
? const Icon(Icons.more_horiz_rounded)
|
||||
: const SizedBox(),
|
||||
onTap: userId == user.id || isOwner
|
||||
onTap: userId == user.uid || isOwner
|
||||
? () => handleUserClick(user)
|
||||
: null,
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
@@ -12,7 +12,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final sharedUsers = useRef<List<User>>(const []);
|
||||
final sharedUsers = useRef<List<UserDto>>(const []);
|
||||
sharedUsers.value = ref.watch(
|
||||
currentAlbumProvider.select((album) {
|
||||
if (album == null) {
|
||||
@@ -23,7 +23,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
|
||||
return sharedUsers.value;
|
||||
}
|
||||
|
||||
return album.sharedUsers.toList(growable: false);
|
||||
return album.sharedUsers.map((u) => u.toDto()).toList(growable: false);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -21,7 +21,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final sharedUsersList = useState<Set<User>>({});
|
||||
final sharedUsersList = useState<Set<UserDto>>({});
|
||||
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
||||
|
||||
createSharedAlbum() async {
|
||||
@@ -48,7 +48,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
buildTileIcon(User user) {
|
||||
buildTileIcon(UserDto user) {
|
||||
if (sharedUsersList.value.contains(user)) {
|
||||
return CircleAvatar(
|
||||
backgroundColor: context.primaryColor,
|
||||
@@ -64,7 +64,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
buildUserList(List<User> users) {
|
||||
buildUserList(List<UserDto> users) {
|
||||
List<Widget> usersChip = [];
|
||||
|
||||
for (var user in sharedUsersList.value) {
|
||||
|
||||
@@ -33,7 +33,7 @@ class AlbumsPage extends HookConsumerWidget {
|
||||
final searchController = useTextEditingController();
|
||||
final debounceTimer = useRef<Timer?>(null);
|
||||
final filterMode = useState(QuickFilterMode.all);
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
final userId = ref.watch(currentUserProvider)?.uid;
|
||||
final searchFocusNode = useFocusNode();
|
||||
|
||||
toggleViewMode() {
|
||||
|
||||
@@ -7,12 +7,12 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_text_field.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_tile.dart';
|
||||
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_text_field.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_tile.dart';
|
||||
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ActivitiesPage extends HookConsumerWidget {
|
||||
@@ -72,7 +72,7 @@ class ActivitiesPage extends HookConsumerWidget {
|
||||
|
||||
final activity = data[index];
|
||||
final canDelete = activity.user.id == user?.id ||
|
||||
album.ownerId == user?.id;
|
||||
album.ownerId == user?.uid;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
@@ -163,7 +163,7 @@ class QuickAccessButtons extends ConsumerWidget {
|
||||
class PartnerList extends ConsumerWidget {
|
||||
const PartnerList({super.key, required this.partners});
|
||||
|
||||
final List<User> partners;
|
||||
final List<UserDto> partners;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||
import 'package:immich_mobile/services/partner.service.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||
@@ -16,7 +16,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final List<User> partners = ref.watch(partnerSharedByProvider);
|
||||
final List<UserDto> partners = ref.watch(partnerSharedByProvider);
|
||||
final availableUsers = ref.watch(partnerAvailableProvider);
|
||||
|
||||
addNewUsersHandler() async {
|
||||
@@ -29,13 +29,13 @@ class PartnerPage extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedUser = await showDialog<User>(
|
||||
final selectedUser = await showDialog<UserDto>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
title: const Text("partner_page_select_partner").tr(),
|
||||
children: [
|
||||
for (User u in users)
|
||||
for (UserDto u in users)
|
||||
SimpleDialogOption(
|
||||
onPressed: () => context.pop(u),
|
||||
child: Row(
|
||||
@@ -67,7 +67,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteUser(User u) {
|
||||
onDeleteUser(UserDto u) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
@@ -80,7 +80,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
buildUserList(List<User> users) {
|
||||
buildUserList(List<UserDto> users) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -15,7 +15,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
class PartnerDetailPage extends HookConsumerWidget {
|
||||
const PartnerDetailPage({super.key, required this.partner});
|
||||
|
||||
final User partner;
|
||||
final UserDto partner;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -111,7 +111,7 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
renderListProvider: singleUserTimelineProvider(partner.isarId),
|
||||
renderListProvider: singleUserTimelineProvider(partner.id),
|
||||
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
|
||||
deleteEnabled: false,
|
||||
favoriteEnabled: false,
|
||||
|
||||
@@ -7,16 +7,16 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline.provider.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PhotosPage extends HookConsumerWidget {
|
||||
@@ -110,7 +110,7 @@ class PhotosPage extends HookConsumerWidget {
|
||||
: const SizedBox(),
|
||||
renderListProvider: timelineUsers.length > 1
|
||||
? multiUsersTimelineProvider(timelineUsers)
|
||||
: singleUserTimelineProvider(currentUser?.isarId),
|
||||
: singleUserTimelineProvider(currentUser?.id),
|
||||
buildLoadingIndicator: buildLoadingIndicator,
|
||||
onRefresh: refreshAssets,
|
||||
stackEnabled: true,
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
|
||||
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
@@ -88,7 +88,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
await albumService.addUsers(album, userIds);
|
||||
}
|
||||
|
||||
Future<bool> removeUser(Album album, User user) async {
|
||||
Future<bool> removeUser(Album album, UserDto user) async {
|
||||
final isRemoved = await albumService.removeUser(album, user);
|
||||
|
||||
if (isRemoved && album.sharedUsers.isEmpty) {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
|
||||
final otherUsersProvider =
|
||||
FutureProvider.autoDispose<List<UserDto>>((ref) async {
|
||||
UserService userService = ref.watch(userServiceProvider);
|
||||
final currentUser = ref.watch(currentUserProvider);
|
||||
|
||||
return userService.getUsers();
|
||||
final allUsers = await userService.getAll();
|
||||
allUsers.removeWhere((u) => currentUser?.id == u.id);
|
||||
return allUsers;
|
||||
});
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:immich_mobile/services/etag.service.dart';
|
||||
import 'package:immich_mobile/services/exif.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
@@ -59,7 +60,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
await clearAllAssets();
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
}
|
||||
final users = await _userService.getUsersFromServer();
|
||||
final users = await _syncService.getUsersFromServer();
|
||||
bool changedUsers = false;
|
||||
if (users != null) {
|
||||
changedUsers = await _syncService.syncUsersFromServer(users);
|
||||
@@ -86,7 +87,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
_assetService.clearTable(),
|
||||
_exifService.clearTable(),
|
||||
_albumService.clearTable(),
|
||||
_userService.clearTable(),
|
||||
_userService.deleteAll(),
|
||||
_etagService.clearTable(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
|
||||
import 'package:immich_mobile/models/auth/auth_state.model.dart';
|
||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
@@ -105,7 +106,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
String deviceId =
|
||||
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
|
||||
|
||||
User? user = Store.tryGet(StoreKey.currentUser);
|
||||
UserDto? user = Store.tryGet(StoreKey.currentUser);
|
||||
|
||||
UserAdminResponseDto? userResponse;
|
||||
UserPreferencesResponseDto? userPreferences;
|
||||
@@ -141,18 +142,18 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
|
||||
// If the user information is successfully retrieved, update the store
|
||||
// Due to the flow of the code, this will always happen on first login
|
||||
if (userResponse != null) {
|
||||
if (userResponse == null) {
|
||||
_log.severe("Unable to get user information from the server.");
|
||||
} else {
|
||||
await Store.put(StoreKey.deviceId, deviceId);
|
||||
await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
|
||||
await Store.put(
|
||||
StoreKey.currentUser,
|
||||
User.fromUserDto(userResponse, userPreferences),
|
||||
UserConverter.fromAdminDto(userResponse, userPreferences),
|
||||
);
|
||||
await Store.put(StoreKey.accessToken, accessToken);
|
||||
|
||||
user = User.fromUserDto(userResponse, userPreferences);
|
||||
} else {
|
||||
_log.severe("Unable to get user information from the server.");
|
||||
user = UserConverter.fromAdminDto(userResponse, userPreferences);
|
||||
}
|
||||
|
||||
// If the user is null, the login was not successful
|
||||
@@ -163,7 +164,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
|
||||
state = state.copyWith(
|
||||
isAuthenticated: true,
|
||||
userId: user.id,
|
||||
userId: user.uid,
|
||||
userEmail: user.email,
|
||||
name: user.name,
|
||||
profileImagePath: user.profileImagePath,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'store.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
@Riverpod(keepAlive: true)
|
||||
IStoreRepository storeRepository(Ref ref) =>
|
||||
IsarStoreRepository(ref.watch(isarProvider));
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
StoreService storeService(Ref _) => StoreService.I;
|
||||
|
||||
@@ -6,11 +6,11 @@ part of 'store.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$storeRepositoryHash() => r'9f378b96e552151fa14a8c8ce2c30a5f38f436ed';
|
||||
String _$storeRepositoryHash() => r'99d24875d30c5e86b1c6caa352a0026167114e62';
|
||||
|
||||
/// See also [storeRepository].
|
||||
@ProviderFor(storeRepository)
|
||||
final storeRepositoryProvider = AutoDisposeProvider<IStoreRepository>.internal(
|
||||
final storeRepositoryProvider = Provider<IStoreRepository>.internal(
|
||||
storeRepository,
|
||||
name: r'storeRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
@@ -22,6 +22,22 @@ final storeRepositoryProvider = AutoDisposeProvider<IStoreRepository>.internal(
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>;
|
||||
typedef StoreRepositoryRef = ProviderRef<IStoreRepository>;
|
||||
String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0';
|
||||
|
||||
/// See also [storeService].
|
||||
@ProviderFor(storeService)
|
||||
final storeServiceProvider = Provider<StoreService>.internal(
|
||||
storeService,
|
||||
name: r'storeServiceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$storeServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef StoreServiceRef = ProviderRef<StoreService>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
||||
27
mobile/lib/providers/infrastructure/user.provider.dart
Normal file
27
mobile/lib/providers/infrastructure/user.provider.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'user.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
IUserRepository userRepository(Ref ref) =>
|
||||
IsarUserRepository(ref.watch(isarProvider));
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
IUserApiRepository userApiRepository(Ref ref) =>
|
||||
UserApiRepository(ref.watch(apiServiceProvider).usersApi);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
UserService userService(Ref ref) => UserService(
|
||||
userRepository: ref.watch(userRepositoryProvider),
|
||||
userApiRepository: ref.watch(userApiRepositoryProvider),
|
||||
storeService: ref.watch(storeServiceProvider),
|
||||
);
|
||||
60
mobile/lib/providers/infrastructure/user.provider.g.dart
generated
Normal file
60
mobile/lib/providers/infrastructure/user.provider.g.dart
generated
Normal file
@@ -0,0 +1,60 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$userRepositoryHash() => r'1a2ac726bcc44397dcaecf449084fefd336696d4';
|
||||
|
||||
/// See also [userRepository].
|
||||
@ProviderFor(userRepository)
|
||||
final userRepositoryProvider = Provider<IUserRepository>.internal(
|
||||
userRepository,
|
||||
name: r'userRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$userRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef UserRepositoryRef = ProviderRef<IUserRepository>;
|
||||
String _$userApiRepositoryHash() => r'6b19f2c99fb83162a5ceb91adb8589eaae01bc92';
|
||||
|
||||
/// See also [userApiRepository].
|
||||
@ProviderFor(userApiRepository)
|
||||
final userApiRepositoryProvider = Provider<IUserApiRepository>.internal(
|
||||
userApiRepository,
|
||||
name: r'userApiRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$userApiRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef UserApiRepositoryRef = ProviderRef<IUserApiRepository>;
|
||||
String _$userServiceHash() => r'4a0873357b7115b4d6bfa8e89b847c0b74ce0d93';
|
||||
|
||||
/// See also [userService].
|
||||
@ProviderFor(userService)
|
||||
final userServiceProvider = Provider<UserService>.internal(
|
||||
userService,
|
||||
name: r'userServiceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$userServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef UserServiceRef = ProviderRef<UserService>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -2,16 +2,16 @@ import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/services/partner.service.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
||||
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
||||
class PartnerSharedWithNotifier extends StateNotifier<List<UserDto>> {
|
||||
final PartnerService _partnerService;
|
||||
late final StreamSubscription<List<User>> streamSub;
|
||||
late final StreamSubscription<List<UserDto>> streamSub;
|
||||
|
||||
PartnerSharedWithNotifier(this._partnerService) : super([]) {
|
||||
Function eq = const ListEquality<User>().equals;
|
||||
Function eq = const ListEquality<UserDto>().equals;
|
||||
_partnerService.getSharedWith().then((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
@@ -25,7 +25,7 @@ class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> updatePartner(User partner, {required bool inTimeline}) {
|
||||
Future<bool> updatePartner(UserDto partner, {required bool inTimeline}) {
|
||||
return _partnerService.updatePartner(partner, inTimeline: inTimeline);
|
||||
}
|
||||
|
||||
@@ -39,18 +39,18 @@ class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
||||
}
|
||||
|
||||
final partnerSharedWithProvider =
|
||||
StateNotifierProvider<PartnerSharedWithNotifier, List<User>>((ref) {
|
||||
StateNotifierProvider<PartnerSharedWithNotifier, List<UserDto>>((ref) {
|
||||
return PartnerSharedWithNotifier(
|
||||
ref.watch(partnerServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
||||
class PartnerSharedByNotifier extends StateNotifier<List<UserDto>> {
|
||||
final PartnerService _partnerService;
|
||||
late final StreamSubscription<List<User>> streamSub;
|
||||
late final StreamSubscription<List<UserDto>> streamSub;
|
||||
|
||||
PartnerSharedByNotifier(this._partnerService) : super([]) {
|
||||
Function eq = const ListEquality<User>().equals;
|
||||
Function eq = const ListEquality<UserDto>().equals;
|
||||
_partnerService.getSharedBy().then((partners) {
|
||||
if (!eq(state, partners)) {
|
||||
state = partners;
|
||||
@@ -74,15 +74,15 @@ class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
||||
}
|
||||
|
||||
final partnerSharedByProvider =
|
||||
StateNotifierProvider<PartnerSharedByNotifier, List<User>>((ref) {
|
||||
StateNotifierProvider<PartnerSharedByNotifier, List<UserDto>>((ref) {
|
||||
return PartnerSharedByNotifier(ref.watch(partnerServiceProvider));
|
||||
});
|
||||
|
||||
final partnerAvailableProvider =
|
||||
FutureProvider.autoDispose<List<User>>((ref) async {
|
||||
FutureProvider.autoDispose<List<UserDto>>((ref) async {
|
||||
final otherUsers = await ref.watch(otherUsersProvider.future);
|
||||
final currentPartners = ref.watch(partnerSharedByProvider);
|
||||
final available = Set<User>.of(otherUsers);
|
||||
final available = Set<UserDto>.of(otherUsers);
|
||||
available.removeAll(currentPartners);
|
||||
return available.toList();
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
|
||||
enum UploadProfileStatus {
|
||||
idle,
|
||||
@@ -72,7 +72,7 @@ class UploadProfileImageState {
|
||||
|
||||
class UploadProfileImageNotifier
|
||||
extends StateNotifier<UploadProfileImageState> {
|
||||
UploadProfileImageNotifier(this._userSErvice)
|
||||
UploadProfileImageNotifier(this._userService)
|
||||
: super(
|
||||
UploadProfileImageState(
|
||||
profileImagePath: '',
|
||||
@@ -80,18 +80,21 @@ class UploadProfileImageNotifier
|
||||
),
|
||||
);
|
||||
|
||||
final UserService _userSErvice;
|
||||
final UserService _userService;
|
||||
|
||||
Future<bool> upload(XFile file) async {
|
||||
state = state.copyWith(status: UploadProfileStatus.loading);
|
||||
|
||||
var res = await _userSErvice.uploadProfileImage(file);
|
||||
var profileImagePath = await _userService.createProfileImage(
|
||||
file.name,
|
||||
await file.readAsBytes(),
|
||||
);
|
||||
|
||||
if (res != null) {
|
||||
if (profileImagePath != null) {
|
||||
debugPrint("Successfully upload profile image");
|
||||
state = state.copyWith(
|
||||
status: UploadProfileStatus.success,
|
||||
profileImagePath: res.profileImagePath,
|
||||
profileImagePath: profileImagePath,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/timeline.service.dart';
|
||||
|
||||
class CurrentUserProvider extends StateNotifier<User?> {
|
||||
class CurrentUserProvider extends StateNotifier<UserDto?> {
|
||||
CurrentUserProvider(this._apiService) : super(null) {
|
||||
state = Store.tryGet(StoreKey.currentUser);
|
||||
streamSub =
|
||||
@@ -16,7 +17,7 @@ class CurrentUserProvider extends StateNotifier<User?> {
|
||||
}
|
||||
|
||||
final ApiService _apiService;
|
||||
late final StreamSubscription<User?> streamSub;
|
||||
late final StreamSubscription<UserDto?> streamSub;
|
||||
|
||||
refresh() async {
|
||||
try {
|
||||
@@ -25,7 +26,7 @@ class CurrentUserProvider extends StateNotifier<User?> {
|
||||
if (user != null) {
|
||||
await Store.put(
|
||||
StoreKey.currentUser,
|
||||
User.fromUserDto(user, userPreferences),
|
||||
UserConverter.fromAdminDto(user, userPreferences),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -39,7 +40,7 @@ class CurrentUserProvider extends StateNotifier<User?> {
|
||||
}
|
||||
|
||||
final currentUserProvider =
|
||||
StateNotifierProvider<CurrentUserProvider, User?>((ref) {
|
||||
StateNotifierProvider<CurrentUserProvider, UserDto?>((ref) {
|
||||
return CurrentUserProvider(
|
||||
ref.watch(apiServiceProvider),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
|
||||
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
@@ -60,7 +60,7 @@ class ActivityApiRepository extends ApiRepository
|
||||
type: dto.type == ReactionType.comment
|
||||
? ActivityType.comment
|
||||
: ActivityType.like,
|
||||
user: User.fromSimpleUserDto(dto.user),
|
||||
user: UserConverter.fromSimpleUserDto(dto.user),
|
||||
assetId: dto.assetId,
|
||||
comment: dto.comment,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
||||
as entity;
|
||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
@@ -43,11 +45,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
}
|
||||
if (owner == true) {
|
||||
query = query.owner(
|
||||
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
|
||||
);
|
||||
} else if (owner == false) {
|
||||
query = query.owner(
|
||||
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
|
||||
);
|
||||
}
|
||||
if (remote == true) {
|
||||
@@ -100,8 +102,9 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
Future<Album?> get(int id) => db.albums.get(id);
|
||||
|
||||
@override
|
||||
Future<void> removeUsers(Album album, List<User> users) =>
|
||||
txn(() => album.sharedUsers.update(unlink: users));
|
||||
Future<void> removeUsers(Album album, List<UserDto> users) => txn(
|
||||
() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto)),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> addAssets(Album album, List<Asset> assets) =>
|
||||
@@ -121,8 +124,8 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addUsers(Album album, List<User> users) =>
|
||||
txn(() => album.sharedUsers.update(link: users));
|
||||
Future<void> addUsers(Album album, List<UserDto> users) =>
|
||||
txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto)));
|
||||
|
||||
@override
|
||||
Future<void> deleteAllLocal() =>
|
||||
@@ -141,11 +144,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
switch (filterMode) {
|
||||
case QuickFilterMode.sharedWithMe:
|
||||
query = query.owner(
|
||||
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
|
||||
);
|
||||
case QuickFilterMode.myAlbums:
|
||||
query = query.owner(
|
||||
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
|
||||
);
|
||||
case QuickFilterMode.all:
|
||||
break;
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
||||
as entity;
|
||||
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
|
||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
@@ -164,11 +166,12 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
||||
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
|
||||
);
|
||||
album.remoteAssetCount = dto.assetCount;
|
||||
album.owner.value = User.fromSimpleUserDto(dto.owner);
|
||||
album.owner.value =
|
||||
entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner));
|
||||
album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
|
||||
final users = dto.albumUsers
|
||||
.map((albumUser) => User.fromSimpleUserDto(albumUser.user));
|
||||
album.sharedUsers.addAll(users);
|
||||
.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user));
|
||||
album.sharedUsers.addAll(users.map(entity.User.fromDto));
|
||||
final assets = dto.assets.map(Asset.remote).toList();
|
||||
album.assets.addAll(assets);
|
||||
return album;
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||
@@ -86,7 +87,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
shared: false,
|
||||
activityEnabled: false,
|
||||
);
|
||||
album.owner.value = Store.get(StoreKey.currentUser);
|
||||
album.owner.value = User.fromDto(Store.get(StoreKey.currentUser));
|
||||
album.localId = assetPathEntity.id;
|
||||
album.isAll = assetPathEntity.isAll;
|
||||
return album;
|
||||
|
||||
@@ -24,7 +24,7 @@ class AssetMediaRepository implements IAssetMediaRepository {
|
||||
final Asset asset = Asset(
|
||||
checksum: "",
|
||||
localId: local.id,
|
||||
ownerId: Store.get(StoreKey.currentUser).isarId,
|
||||
ownerId: Store.get(StoreKey.currentUser).id,
|
||||
fileCreatedAt: local.createDateTime,
|
||||
fileModifiedAt: local.modifiedDateTime,
|
||||
updatedAt: local.modifiedDateTime,
|
||||
|
||||
@@ -6,8 +6,8 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
||||
as entity;
|
||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
@@ -14,34 +16,40 @@ class PartnerRepository extends DatabaseRepository
|
||||
PartnerRepository(super.db);
|
||||
|
||||
@override
|
||||
Future<List<User>> getSharedBy() {
|
||||
return db.users
|
||||
.filter()
|
||||
.isPartnerSharedByEqualTo(true)
|
||||
.sortById()
|
||||
.findAll();
|
||||
Future<List<UserDto>> getSharedBy() async {
|
||||
return (await db.users
|
||||
.filter()
|
||||
.isPartnerSharedByEqualTo(true)
|
||||
.sortById()
|
||||
.findAll())
|
||||
.map((u) => u.toDto())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<User>> getSharedWith() {
|
||||
return db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.sortById()
|
||||
.findAll();
|
||||
Future<List<UserDto>> getSharedWith() async {
|
||||
return (await db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.sortById()
|
||||
.findAll())
|
||||
.map((u) => u.toDto())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<User>> watchSharedBy() {
|
||||
return db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch();
|
||||
Stream<List<UserDto>> watchSharedBy() {
|
||||
return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch())
|
||||
.map((users) => users.map((u) => u.toDto()).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<User>> watchSharedWith() {
|
||||
return db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.sortById()
|
||||
.watch();
|
||||
Stream<List<UserDto>> watchSharedWith() {
|
||||
return (db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.sortById()
|
||||
.watch())
|
||||
.map((users) => users.map((u) => u.toDto()).toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
@@ -18,7 +19,7 @@ class PartnerApiRepository extends ApiRepository
|
||||
PartnerApiRepository(this._api);
|
||||
|
||||
@override
|
||||
Future<List<User>> getAll(Direction direction) async {
|
||||
Future<List<UserDto>> getAll(Direction direction) async {
|
||||
final response = await checkNull(
|
||||
_api.getPartners(
|
||||
direction == Direction.sharedByMe
|
||||
@@ -26,26 +27,26 @@ class PartnerApiRepository extends ApiRepository
|
||||
: PartnerDirection.with_,
|
||||
),
|
||||
);
|
||||
return response.map(User.fromPartnerDto).toList();
|
||||
return response.map(UserConverter.fromPartnerDto).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> create(String id) async {
|
||||
Future<UserDto> create(String id) async {
|
||||
final dto = await checkNull(_api.createPartner(id));
|
||||
return User.fromPartnerDto(dto);
|
||||
return UserConverter.fromPartnerDto(dto);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) => _api.removePartner(id);
|
||||
|
||||
@override
|
||||
Future<User> update(String id, {required bool inTimeline}) async {
|
||||
Future<UserDto> update(String id, {required bool inTimeline}) async {
|
||||
final dto = await checkNull(
|
||||
_api.updatePartner(
|
||||
id,
|
||||
UpdatePartnerDto(inTimeline: inTimeline),
|
||||
),
|
||||
);
|
||||
return User.fromPartnerDto(dto);
|
||||
return UserConverter.fromPartnerDto(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/timeline.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user